June 8, 2022

Decorators and forwarding, call/apply

JavaScript gives exceptional flexibility when dealing with functions. They can be passed around, used as objects, and now we’ll see how to forward calls between them and decorate them.

Transparent caching

Let’s say we have a function slow(x) which is CPU-heavy, but its results are stable. In other words, for the same x it always returns the same result.

If the function is called often, we may want to cache (remember) the results to avoid spending extra-time on recalculations.

But instead of adding that functionality into slow() we’ll create a wrapper function, that adds caching. As we’ll see, there are many benefits of doing so.

Here’s the code, and explanations follow:

function slow(x) {
  // there can be a heavy CPU-intensive job here
  alert(`Called with ${x}`);
  return x;
}

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

  return function(x) {
    if (cache.has(x)) {    // if there's such key in cache
      return cache.get(x); // read the result from it
    }

    let result = func(x);  // otherwise call func

    cache.set(x, result);  // and cache (remember) the result
    return result;
  };
}

slow = cachingDecorator(slow);

alert( slow(1) ); // slow(1) is cached and the result returned
alert( "Again: " + slow(1) ); // slow(1) result returned from cache

alert( slow(2) ); // slow(2) is cached and the result returned
alert( "Again: " + slow(2) ); // slow(2) result returned from cache

In the code above cachingDecorator is a decorator: a special function that takes another function and alters its behavior.

The idea is that we can call cachingDecorator for any function, and it will return the caching wrapper. That’s great, because we can have many functions that could use such a feature, and all we need to do is to apply cachingDecorator to them.

By separating caching from the main function code we also keep the main code simpler.

The result of cachingDecorator(func) is a “wrapper”: function(x) that “wraps” the call of func(x) into caching logic:

From an outside code, the wrapped slow function still does the same. It just got a caching aspect added to its behavior.

To summarize, there are several benefits of using a separate cachingDecorator instead of altering the code of slow itself:

  • The cachingDecorator is reusable. We can apply it to another function.
  • The caching logic is separate, it did not increase the complexity of slow itself (if there was any).
  • We can combine multiple decorators if needed (other decorators will follow).

Using “func.call” for the context

The caching decorator mentioned above is not suited to work with object methods.

For instance, in the code below worker.slow() stops working after the decoration:

// we'll make worker.slow caching
let worker = {
  someMethod() {
    return 1;
  },

  slow(x) {
    // scary CPU-heavy task here
    alert("Called with " + x);
    return x * this.someMethod(); // (*)
  }
};

// same code as before
function cachingDecorator(func) {
  let cache = new Map();
  return function(x) {
    if (cache.has(x)) {
      return cache.get(x);
    }
    let result = func(x); // (**)
    cache.set(x, result);
    return result;
  };
}

alert( worker.slow(1) ); // the original method works

worker.slow = cachingDecorator(worker.slow); // now make it caching

alert( worker.slow(2) ); // Whoops! Error: Cannot read property 'someMethod' of undefined

The error occurs in the line (*) that tries to access this.someMethod and fails. Can you see why?

The reason is that the wrapper calls the original function as func(x) in the line (**). And, when called like that, the function gets this = undefined.

We would observe a similar symptom if we tried to run:

let func = worker.slow;
func(2);

So, the wrapper passes the call to the original method, but without the context this. Hence the error.

Let’s fix it.

There’s a special built-in function method func.call(context, …args) that allows to call a function explicitly setting this.

The syntax is:

func.call(context, arg1, arg2, ...)

It runs func providing the first argument as this, and the next as the arguments.

To put it simply, these two calls do almost the same:

func(1, 2, 3);
func.call(obj, 1, 2, 3)

They both call func with arguments 1, 2 and 3. The only difference is that func.call also sets this to obj.

As an example, in the code below we call sayHi in the context of different objects: sayHi.call(user) runs sayHi providing this=user, and the next line sets this=admin:

function sayHi() {
  alert(this.name);
}

let user = { name: "John" };
let admin = { name: "Admin" };

// use call to pass different objects as "this"
sayHi.call( user ); // John
sayHi.call( admin ); // Admin

And here we use call to call say with the given context and phrase:

function say(phrase) {
  alert(this.name + ': ' + phrase);
}

let user = { name: "John" };

// user becomes this, and "Hello" becomes the first argument
say.call( user, "Hello" ); // John: Hello

In our case, we can use call in the wrapper to pass the context to the original function:

let worker = {
  someMethod() {
    return 1;
  },

  slow(x) {
    alert("Called with " + x);
    return x * this.someMethod(); // (*)
  }
};

function cachingDecorator(func) {
  let cache = new Map();
  return function(x) {
    if (cache.has(x)) {
      return cache.get(x);
    }
    let result = func.call(this, x); // "this" is passed correctly now
    cache.set(x, result);
    return result;
  };
}

worker.slow = cachingDecorator(worker.slow); // now make it caching

alert( worker.slow(2) ); // works
alert( worker.slow(2) ); // works, doesn't call the original (cached)

Now everything is fine.

To make it all clear, let’s see more deeply how this is passed along:

  1. After the decoration worker.slow is now the wrapper function (x) { ... }.
  2. So when worker.slow(2) is executed, the wrapper gets 2 as an argument and this=worker (it’s the object before dot).
  3. Inside the wrapper, assuming the result is not yet cached, func.call(this, x) passes the current this (=worker) and the current argument (=2) to the original method.

Going multi-argument

Now let’s make cachingDecorator even more universal. Till now it was working only with single-argument functions.

Now how to cache the multi-argument worker.slow method?

let worker = {
  slow(min, max) {
    return min + max; // scary CPU-hogger is assumed
  }
};

// should remember same-argument calls
worker.slow = cachingDecorator(worker.slow);

Previously, for a single argument x we could just cache.set(x, result) to save the result and cache.get(x) to retrieve it. But now we need to remember the result for a combination of arguments (min,max). The native Map takes single value only as the key.

There are many solutions possible:

  1. Implement a new (or use a third-party) map-like data structure that is more versatile and allows multi-keys.
  2. Use nested maps: cache.set(min) will be a Map that stores the pair (max, result). So we can get result as cache.get(min).get(max).
  3. Join two values into one. In our particular case we can just use a string "min,max" as the Map key. For flexibility, we can allow to provide a hashing function for the decorator, that knows how to make one value from many.

For many practical applications, the 3rd variant is good enough, so we’ll stick to it.

Also we need to pass not just x, but all arguments in func.call. Let’s recall that in a function() we can get a pseudo-array of its arguments as arguments, so func.call(this, x) should be replaced with func.call(this, ...arguments).

Here’s a more powerful cachingDecorator:

let worker = {
  slow(min, max) {
    alert(`Called with ${min},${max}`);
    return min + max;
  }
};

function cachingDecorator(func, hash) {
  let cache = new Map();
  return function() {
    let key = hash(arguments); // (*)
    if (cache.has(key)) {
      return cache.get(key);
    }

    let result = func.call(this, ...arguments); // (**)

    cache.set(key, result);
    return result;
  };
}

function hash(args) {
  return args[0] + ',' + args[1];
}

worker.slow = cachingDecorator(worker.slow, hash);

alert( worker.slow(3, 5) ); // works
alert( "Again " + worker.slow(3, 5) ); // same (cached)

Now it works with any number of arguments (though the hash function would also need to be adjusted to allow any number of arguments. An interesting way to handle this will be covered below).

There are two changes:

  • In the line (*) it calls hash to create a single key from arguments. Here we use a simple “joining” function that turns arguments (3, 5) into the key "3,5". More complex cases may require other hashing functions.
  • Then (**) uses func.call(this, ...arguments) to pass both the context and all arguments the wrapper got (not just the first one) to the original function.

func.apply

Instead of func.call(this, ...arguments) we could use func.apply(this, arguments).

The syntax of built-in method func.apply is:

func.apply(context, args)

It runs the func setting this=context and using an array-like object args as the list of arguments.

The only syntax difference between call and apply is that call expects a list of arguments, while apply takes an array-like object with them.

So these two calls are almost equivalent:

func.call(context, ...args);
func.apply(context, args);

They perform the same call of func with given context and arguments.

There’s only a subtle difference regarding args:

  • The spread syntax ... allows to pass iterable args as the list to call.
  • The apply accepts only array-like args.

…And for objects that are both iterable and array-like, such as a real array, we can use any of them, but apply will probably be faster, because most JavaScript engines internally optimize it better.

Passing all arguments along with the context to another function is called call forwarding.

That’s the simplest form of it:

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

When an external code calls such wrapper, it is indistinguishable from the call of the original function func.

Borrowing a method

Now let’s make one more minor improvement in the hashing function:

function hash(args) {
  return args[0] + ',' + args[1];
}

As of now, it works only on two arguments. It would be better if it could glue any number of args.

The natural solution would be to use arr.join method:

function hash(args) {
  return args.join();
}

…Unfortunately, that won’t work. Because we are calling hash(arguments), and arguments object is both iterable and array-like, but not a real array.

So calling join on it would fail, as we can see below:

function hash() {
  alert( arguments.join() ); // Error: arguments.join is not a function
}

hash(1, 2);

Still, there’s an easy way to use array join:

function hash() {
  alert( [].join.call(arguments) ); // 1,2
}

hash(1, 2);

The trick is called method borrowing.

We take (borrow) a join method from a regular array ([].join) and use [].join.call to run it in the context of arguments.

Why does it work?

That’s because the internal algorithm of the native method arr.join(glue) is very simple.

Taken from the specification almost “as-is”:

  1. Let glue be the first argument or, if no arguments, then a comma ",".
  2. Let result be an empty string.
  3. Append this[0] to result.
  4. Append glue and this[1].
  5. Append glue and this[2].
  6. …Do so until this.length items are glued.
  7. Return result.

So, technically it takes this and joins this[0], this[1] …etc together. It’s intentionally written in a way that allows any array-like this (not a coincidence, many methods follow this practice). That’s why it also works with this=arguments.

Decorators and function properties

It is generally safe to replace a function or a method with a decorated one, except for one little thing. If the original function had properties on it, like func.calledCount or whatever, then the decorated one will not provide them. Because that is a wrapper. So one needs to be careful if one uses them.

E.g. in the example above if slow function had any properties on it, then cachingDecorator(slow) is a wrapper without them.

Some decorators may provide their own properties. E.g. a decorator may count how many times a function was invoked and how much time it took, and expose this information via wrapper properties.

There exists a way to create decorators that keep access to function properties, but this requires using a special Proxy object to wrap a function. We’ll discuss it later in the article Proxy and Reflect.

Summary

Decorator is a wrapper around a function that alters its behavior. The main job is still carried out by the function.

Decorators can be seen as “features” or “aspects” that can be added to a function. We can add one or add many. And all this without changing its code!

To implement cachingDecorator, we studied methods:

The generic call forwarding is usually done with apply:

let wrapper = function() {
  return original.apply(this, arguments);
};

We also saw an example of method borrowing when we take a method from an object and call it in the context of another object. It is quite common to take array methods and apply them to arguments. The alternative is to use rest parameters object that is a real array.

There are many decorators there in the wild. Check how well you got them by solving the tasks of this chapter.

Tasks

importance: 5

Create a decorator spy(func) that should return a wrapper that saves all calls to function in its calls property.

Every call is saved as an array of arguments.

For instance:

function work(a, b) {
  alert( a + b ); // work is an arbitrary function or method
}

work = spy(work);

work(1, 2); // 3
work(4, 5); // 9

for (let args of work.calls) {
  alert( 'call:' + args.join() ); // "call:1,2", "call:4,5"
}

P.S. That decorator is sometimes useful for unit-testing. Its advanced form is sinon.spy in Sinon.JS library.

Open a sandbox with tests.

The wrapper returned by spy(f) should store all arguments and then use f.apply to forward the call.

function spy(func) {

  function wrapper(...args) {
    // using ...args instead of arguments to store "real" array in wrapper.calls
    wrapper.calls.push(args);
    return func.apply(this, args);
  }

  wrapper.calls = [];

  return wrapper;
}

Open the solution with tests in a sandbox.

importance: 5

Create a decorator delay(f, ms) that delays each call of f by ms milliseconds.

For instance:

function f(x) {
  alert(x);
}

// create wrappers
let f1000 = delay(f, 1000);
let f1500 = delay(f, 1500);

f1000("test"); // shows "test" after 1000ms
f1500("test"); // shows "test" after 1500ms

In other words, delay(f, ms) returns a “delayed by ms” variant of f.

In the code above, f is a function of a single argument, but your solution should pass all arguments and the context this.

Open a sandbox with tests.

The solution:

function delay(f, ms) {

  return function() {
    setTimeout(() => f.apply(this, arguments), ms);
  };

}

let f1000 = delay(alert, 1000);

f1000("test"); // shows "test" after 1000ms

Please note how an arrow function is used here. As we know, arrow functions do not have own this and arguments, so f.apply(this, arguments) takes this and arguments from the wrapper.

If we pass a regular function, setTimeout would call it without arguments and this=window (assuming we’re in the browser).

We still can pass the right this by using an intermediate variable, but that’s a little bit more cumbersome:

function delay(f, ms) {

  return function(...args) {
    let savedThis = this; // store this into an intermediate variable
    setTimeout(function() {
      f.apply(savedThis, args); // use it here
    }, ms);
  };

}

Open the solution with tests in a sandbox.

importance: 5

The result of debounce(f, ms) decorator is a wrapper that suspends calls to f until there’s ms milliseconds of inactivity (no calls, “cooldown period”), then invokes f once with the latest arguments.

In other words, debounce is like a secretary that accepts “phone calls”, and waits until there’s ms milliseconds of being quiet. And only then it transfers the latest call information to “the boss” (calls the actual f).

For instance, we had a function f and replaced it with f = debounce(f, 1000).

Then if the wrapped function is called at 0ms, 200ms and 500ms, and then there are no calls, then the actual f will be only called once, at 1500ms. That is: after the cooldown period of 1000ms from the last call.

…And it will get the arguments of the very last call, other calls are ignored.

Here’s the code for it (uses the debounce decorator from the Lodash library):

let f = _.debounce(alert, 1000);

f("a");
setTimeout( () => f("b"), 200);
setTimeout( () => f("c"), 500);
// debounced function waits 1000ms after the last call and then runs: alert("c")

Now a practical example. Let’s say, the user types something, and we’d like to send a request to the server when the input is finished.

There’s no point in sending the request for every character typed. Instead we’d like to wait, and then process the whole result.

In a web-browser, we can setup an event handler – a function that’s called on every change of an input field. Normally, an event handler is called very often, for every typed key. But if we debounce it by 1000ms, then it will be only called once, after 1000ms after the last input.

In this live example, the handler puts the result into a box below, try it:

See? The second input calls the debounced function, so its content is processed after 1000ms from the last input.

So, debounce is a great way to process a sequence of events: be it a sequence of key presses, mouse movements or something else.

It waits the given time after the last call, and then runs its function, that can process the result.

The task is to implement debounce decorator.

Hint: that’s just a few lines if you think about it :)

Open a sandbox with tests.

function debounce(func, ms) {
  let timeout;
  return function() {
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(this, arguments), ms);
  };
}

A call to debounce returns a wrapper. When called, it schedules the original function call after given ms and cancels the previous such timeout.

Open the solution with tests in a sandbox.

importance: 5

Create a “throttling” decorator throttle(f, ms) – that returns a wrapper.

When it’s called multiple times, it passes the call to f at maximum once per ms milliseconds.

Compared to the debounce decorator, the behavior is completely different:

  • debounce runs the function once after the “cooldown” period. Good for processing the final result.
  • throttle runs it not more often than given ms time. Good for regular updates that shouldn’t be very often.

In other words, throttle is like a secretary that accepts phone calls, but bothers the boss (calls the actual f) not more often than once per ms milliseconds.

Let’s check the real-life application to better understand that requirement and to see where it comes from.

For instance, we want to track mouse movements.

In a browser we can setup a function to run at every mouse movement and get the pointer location as it moves. During an active mouse usage, this function usually runs very frequently, can be something like 100 times per second (every 10 ms). We’d like to update some information on the web-page when the pointer moves.

…But updating function update() is too heavy to do it on every micro-movement. There is also no sense in updating more often than once per 100ms.

So we’ll wrap it into the decorator: use throttle(update, 100) as the function to run on each mouse move instead of the original update(). The decorator will be called often, but forward the call to update() at maximum once per 100ms.

Visually, it will look like this:

  1. For the first mouse movement the decorated variant immediately passes the call to update. That’s important, the user sees our reaction to their move immediately.
  2. Then as the mouse moves on, until 100ms nothing happens. The decorated variant ignores calls.
  3. At the end of 100ms – one more update happens with the last coordinates.
  4. Then, finally, the mouse stops somewhere. The decorated variant waits until 100ms expire and then runs update with last coordinates. So, quite important, the final mouse coordinates are processed.

A code example:

function f(a) {
  console.log(a);
}

// f1000 passes calls to f at maximum once per 1000 ms
let f1000 = throttle(f, 1000);

f1000(1); // shows 1
f1000(2); // (throttling, 1000ms not out yet)
f1000(3); // (throttling, 1000ms not out yet)

// when 1000 ms time out...
// ...outputs 3, intermediate value 2 was ignored

P.S. Arguments and the context this passed to f1000 should be passed to the original f.

Open a sandbox with tests.

function throttle(func, ms) {

  let isThrottled = false,
    savedArgs,
    savedThis;

  function wrapper() {

    if (isThrottled) { // (2)
      savedArgs = arguments;
      savedThis = this;
      return;
    }
    isThrottled = true;

    func.apply(this, arguments); // (1)

    setTimeout(function() {
      isThrottled = false; // (3)
      if (savedArgs) {
        wrapper.apply(savedThis, savedArgs);
        savedArgs = savedThis = null;
      }
    }, ms);
  }

  return wrapper;
}

A call to throttle(func, ms) returns wrapper.

  1. During the first call, the wrapper just runs func and sets the cooldown state (isThrottled = true).
  2. In this state all calls are memorized in savedArgs/savedThis. Please note that both the context and the arguments are equally important and should be memorized. We need them simultaneously to reproduce the call.
  3. After ms milliseconds pass, setTimeout triggers. The cooldown state is removed (isThrottled = false) and, if we had ignored calls, wrapper is executed with the last memorized arguments and context.

The 3rd step runs not func, but wrapper, because we not only need to execute func, but once again enter the cooldown state and setup the timeout to reset it.

Open the solution with tests in a sandbox.

Tutorial map