Skip to content

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:

js
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

js
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:

  1. We create a Map called cache to store results.
  2. We return a new function that checks if the result for the argument x is already cached.
  3. If the result exists in the cache, it's returned directly.
  4. If not, the original function (func(x)) is called, the result is cached, and then the result is returned.

Applying the Decorator to slow

js
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 the slow 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.

js
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.

js
func.call(context, arg1, arg2, ...);
  • context: The value of this that you want to bind.
  • arg1, arg2, ...: Arguments to pass to the function.
js
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.

js
func.apply(context, [argsArray]);
js
function say(phrase) {
    alert(this.name + ": " + phrase);
}

let user = { name: "John" };
say.apply(user, ["Hello"]);  // John: Hello

This is equivalent to calling:

js
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.

js
let boundFunction = func.bind(context, arg1, arg2, ...);
js
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() and apply() are used to invoke a function with a specific this context, but apply() takes an array of arguments while call() takes arguments one by one.
  • bind() creates a new function with a fixed this 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.

Made with ❤️ for students, by a fellow learner.