Decorators and function manipulation via methods like call
, apply
, and bind
.
1. Decorators in JavaScript
Decorators in JavaScript are a design pattern that allows you to add behavior to an existing function without modifying its structure.
Transparent Caching Decorator
using a caching decorator to cache the results of a function call.
Original slow
Function:
function slow(x) {
// Simulate a slow process (e.g., network request, heavy calculation)
alert(`called with ${x}`);
return x;
}
The function slow(x)
simulates some slow operation and returns the value passed to it. We want to enhance it by caching the results of function calls to avoid redundant processing.
Caching Decorator
function cachingDecorator(func) { // decorator that takes another function
let cache = new Map(); // cache to store computed results
return function(x) { // returns a new function
if (cache.has(x)) { // check if result is cached
return cache.get(x); // return the cached result
}
let result = func(x); // otherwise, call the original function
cache.set(x, result); // store the result in the cache
return result; // return the computed result
};
}
In this cachingDecorator
:
- We create a Map called
cache
to store results. - We return a new function that checks if the result for the argument
x
is already cached. - If the result exists in the cache, it's returned directly.
- If not, the original function (
func(x)
) is called, the result is cached, and then the result is returned.
Applying the Decorator to slow
slow = cachingDecorator(slow);
alert(slow(1)); // First time, slow(1) is computed and cached
alert("Again: " + slow(1)); // Cached result returned
alert(slow(2)); // First time, slow(2) is computed and cached
alert("Again: " + slow(2)); // Cached result returned
In this code:
- When you call
slow(1)
for the first time, the decorator caches the result. - On subsequent calls with the same argument (like
slow(1)
again), the result is retrieved from the cache without re-running theslow
function.
Limitation of Caching Decorators
As you pointed out, the caching decorator may not work well with object methods. This is because when you apply a decorator to a method, it may break the this
context, which is particularly important for methods in objects.
let obj = {
name: "Obj",
slow: function(x) {
alert('called with ' + x);
return x;
}
};
obj.slow = cachingDecorator(obj.slow);
obj.slow(1); // Will not work properly if we need to access `this.name` in the method
To make the caching decorator work with object methods, you'd need to preserve the this
context, but that's a more advanced use case involving bind
.
2. call()
, apply()
, and bind()
Now, let's go over the call
, apply
, and bind
methods — they are all used to change the this
context inside a function.
call()
The call()
method allows you to explicitly set the this
value in a function. It also allows you to pass arguments one by one.
func.call(context, arg1, arg2, ...);
context
: The value ofthis
that you want to bind.arg1, arg2, ...
: Arguments to pass to the function.
function sayHi() {
alert(this.name);
}
let user = { name: "John" };
let admin = { name: "Admin" };
sayHi.call(user); // John
sayHi.call(admin); // Admin
In this case, calling sayHi.call(user)
changes the this
inside sayHi
to refer to the user
object, and similarly for admin
.
If we call sayHi()
directly, it would use the global this
(which, in non-strict mode, would be the window
object in browsers).
apply()
The apply()
method is very similar to call()
, but instead of passing arguments one by one, you pass them as an array.
func.apply(context, [argsArray]);
function say(phrase) {
alert(this.name + ": " + phrase);
}
let user = { name: "John" };
say.apply(user, ["Hello"]); // John: Hello
This is equivalent to calling:
say.call(user, "Hello");
apply
is useful when you have an array or a list of arguments that you want to pass to the function.
bind()
The bind()
method is similar to call
and apply
, but instead of immediately invoking the function, it returns a new function with this
bound to a specific value and optionally pre-defined arguments.
let boundFunction = func.bind(context, arg1, arg2, ...);
function say(phrase) {
alert(this.name + ": " + phrase);
}
let user = { name: "John" };
let boundSay = say.bind(user); // Creates a new function with `this` set to `user`
boundSay("Hello"); // John: Hello
- The
bind()
method does not invoke the function immediately. - You can also pass additional arguments to
bind()
, which will be prepended to the arguments when the function is called.
This is useful for situations where you need to create a function that can be executed later but already has a fixed this
value.
https://javascript.info/call-apply-decorators
func.apply(context, args)
https://javascript.info/bindfunc.bind(user)
Summary:
call()
andapply()
are used to invoke a function with a specificthis
context, butapply()
takes an array of arguments whilecall()
takes arguments one by one.bind()
creates a new function with a fixedthis
value and optional arguments, but does not invoke the function immediately.- Decorators like your caching example allow you to enhance the behavior of existing functions, and they can be used to add features like memoization (caching), logging, or validation.