Decorators and forwarding, call/apply

Sunil Sharma
Last Updated: May 13, 2022

Introduction

JavaScript provides its users great flexibility when it comes to functions. You can use them as an object and pass them around. Decorating your function and forwarding is something you need to know when you play with functions in JavaScript. We will see how to use forwarding with functions in this blog and why you need to decorate your function.

 

Let’s get started by knowing some key terms related to this topic.

Important Key Terms

 

CPU-heavy function: If your function involves too much computation and the CPU is dedicated a lot of time to execute your function, it is called CPU heavy function.

 

Caching: Caching is a technique to store the result for a specific task in the cache memory so that the CPU does not have to calculate the outcome again and again. It simply fetches the result from the cache.

 

You only cache your result if the value of your result is not changing with time. After understanding caching, let’s understand what transparent caching is and where it is useful.

Transparent Caching and Decorators

 

Decorator: Their job is to take a function as an argument and alter its nature according to user needs. You don’t want them to be a part of core functionality. Still, you can wrap core functionalities of the original function in the decorator function to add some new capabilities when you need them.

 

Note- If you make your decorator function generic, which can be used for different functions, your code will become reusable, look clean, and reduce your efforts.

 

Transparent caching: Let’s assume you code a computation-heavy or CPU-heavy function, but the function’s outcome for the same inputs is not changing with time. So instead of recalculating the function, it’s better to store the result somewhere, and every time you call that function, the CPU simply checks whether your input’s result already exists or not. If it exists, the CPU will directly fetch the result. Otherwise, it first calculates your function, then stores that result for future use, and gives it back to you. 

 

Let’s understand this concept with an example.

function slowFun(x) {
  // some CPU intensive code is present
  console.log(`I am slow.`);
  return x;
}
function cachingDecorator(myFun) {
  let cache = new Map();

  return function(x) {
    if (cache.has(x)) {    // if key exist return result from cache
      return cache.get(x); // read the result from cache
    }

    let outcome = myFun(x);  // otherwise call myFun

    cache.set(x, outcome);  // store the result in cache
    return outcome;        // return result 
  };
}
slowFun = cachingDecorator(slowFun);

console.log( slowFun(1) ); // Result for 1  cached and returned
console.log( "Again: " + slowFun(1) ); // Returned result from cache

console.log( slowFun(2) ); //Result for 2  cached and returned
console.log( "Again: " + slowFun(2) ); // Returned result from cache

 

In the example mentioned earlier, we call “cachingDecorator” ( decorator function) for our function and return the caching wrapper.

 

The benefit of writing our code like this is that we can use this decorator for other functions. All we have to do is call “cachingDecorator” for them. In this way, our code is much simpler, manageable, and reusable.

 

Another reason for writing the code like this instead of adding the caching logic in “slowFun” itself is because we can add multiple decorators following one after the other.

The func.call Method ( To Provide Context )

There is a problem with our decorator mentioned above as it will give an error while working with object methods.

 

let country = {
  random() {
    return 1;
  },
  slowFun(x) {        // Function we are trying to cache  
    // some CPU intensive code is present
    console.log("Calling with " + x);

    return x * this.random(); // (Line causing error)- #

  }
};

function cachingDecorator(myFun) {
  let cache = new Map();

  return function(x) {
    if (cache.has(x)) {    // if key exist return result from cache
      return cache.get(x); // read the result from cache
    }

    let outcome = myFun(x);  // otherwise call func - ##
    cache.set(x, outcome);  // store the result in cache
    return outcome;        // return result 
  };
}
console.log( country.slowFun(1) ); 
country.slowFun = cachingDecorator(country.slowFun); 

console.log( country.slowFun(2) ); //Error: Cannot read property 'random' of undefined

 

The problem with the code as mentioned earlier is that when you are trying to call “myFun” you call it directly, and when you do so, “this”  becomes undefined automatically.

 

So how to fix it ?

 

The solution is to use the built-in function method func.call(context, argument) . This method gives us the freedom to call a function without setting this.”

 

Syntax-    func.call(context , argument1, argument2, argument3, ……………)

 

Example- func.call(myObj, 10, 20, 30)

 

let country = {
  
  random() {
    return 1;
  },

  slowFun(x) {        // Function we are trying to cache
    
    // some CPU intensive code is present
    console.log("Calling with " + x);
    return x * this.random(); // (*)
  }
};

function cachingDecorator(myFun) {
  let cache = new Map();

  return function(x) {
    if (cache.has(x)) {    // if key exist return result from cache
      return cache.get(x); // read the result from cache
    }

    let outcome = myFun.call(this,x);  // otherwise call func - ##
    cache.set(x, outcome);  // store the result in cache
    return outcome;        // return result 
  };
}

console.log( country.slowFun(1) ); 

country.slowFun = cachingDecorator(country.slowFun); 

console.log( country.slowFun(2) ); 

 

Now, this code works fine. We provided context which is undefined in earlier code.

The func.apply method 

This is another built-in function method where we can use func.apply(context, arg) instead of func.call(context,...arg).

 

Syntax- func.apply(context, arguments).

 

Example- func.apply(myObj, arguments)

 

The most straightforward code will look like this.

 

 

let wrapper = function() {
  return func.apply(this, arg);
};

 

Difference- The two minor differences between these two methods are

 

  1.  Func.call expects context along with a list of arguments, whereas func.apply expects context along with an array-like object.
  2. You can pass iterable arguments in func.call because the spread operator(...) allows you to do that.

 

Faster- func.apply will work faster in some cases because it is optimized internally by JavaScript engines.

 

Forwarding- The technique to pass arguments along with context to another function is called forwarding. 

Method Borrowing

As the name suggests, method borrowing is to borrow a method, but the question is from where? So in this technique, we borrow a method of another object written earlier by that object so that we do not have to do the same work again and again.

 

How do we do it?

 

We do this with the help of a predefined JavaScript method, which we saw earlier call() and apply().

 

Let’s clear this concept with an example.

let address={
  city: "Panipat",
  state: "Haryana",
  country: "India",
  printAddress: function(){
    console.log(this.city + ", " + this.state + ", " + this.country );
  }
}

address.printAddress();
let address2={
  city: "Mumbai",
  state: "Maharashtra",
  country: "India",
}
address.printAddress.call(address2) // borrowing method of address object
address.printAddress.apply(address2)

Output:

"Panipat, Haryana, India"

"Mumbai, Maharashtra, India"

"Mumbai, Maharashtra, India"

  

Here in the code mentioned above, we are borrowing the method of object “address” and using it for “address2” by passing the context of address2 using call() and apply() method.

Frequently Asked Question

 

How to create an object in JavaScript?

Answer -  we can create objects in JavaScript in three ways-

a)  By using object literal.

b)  By using an object constructor.

c)  By creating an instance of an object.

 

Which is faster JavaScript or ASP script?

Answer- JavaScript is faster because it does not require a web server’s support for its execution.

 

Difference between ‘==’ and ‘===’ operator in JavaScript?

Answer- ‘==’ operator checks the only value of both sides whereas ‘===’ checks a value and the data type of both sides.

 

Is JavaScript a dynamically typed language or loosely typed language?  

Answer- JavaScript is a loosely typed language, so we do not have to assign a type to our variable during compile time. It is automatically assigned to it during runtime.

 

Difference between null and undefined? 

Answer- Undefined means that we declared a variable but have not given a value to it. On the other hand, null is an assignment value. Undefined is a type, whereas null is an object.

Key Takeaways

This blog contains information about forwarding in JavaScript, call() and apply() method in JavaScript.

 

Now, If you are someone who is interested in JavaScript and want to become a great web developer, check out this amazing JavaScript course on web development.

 

Also, if you are preparing for interviews, visit this JavaScript interview question blog.

Happy Reading!

 

Was this article helpful ?
0 upvotes

Comments

No comments yet

Be the first to share what you think