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
- Func.call expects context along with a list of arguments, whereas func.apply expects context along with an array-like object.
- 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.
You can implement it on an online javascript editor.
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!