Decorators and forwarding, call/apply

Functions in JavaScript bend in ways that surprise people coming from stricter languages. You can pass a function as an argument, store it in an object, hand it back from another function, and attach properties to it. This chapter uses that flexibility for one specific move: taking a function and forwarding its calls through a second function that quietly adds a feature. That second function is a decorator.

The payoff is separation. The original function keeps doing its one job. The extra behavior — caching, logging, throttling — lives somewhere else, reusable, and you can stack several of them on the same function without editing a single line of the original.

Transparent caching

Picture a function heavyFee(x) that grinds the CPU to produce its answer, but the answer only depends on x. Same input, same output, every time. That property is what makes caching safe here — a function whose result drifts (say it reads the clock or a database) can’t be cached this way without risking stale answers.

If heavyFee gets called with the same value over and over, recomputing each time is wasted work. You could store previous results and hand them back instantly on a repeat. The naive approach is to bolt that storage logic straight into heavyFee. Resist it. Instead, build a separate wrapper that adds caching around any function you give it.

function heavyFee(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;
  };
}

heavyFee = cachingDecorator(heavyFee);

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

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

cachingDecorator is the decorator: a function that takes a function and returns a new one with altered behavior. Call it on heavyFee and you get back a wrapper. Reassigning heavyFee = cachingDecorator(heavyFee) swaps the wrapper into the old name, so every later heavyFee(...) call runs through the cache.

The cache is a Map that lives in the closure of the returned function. Nobody outside can touch it, but the wrapper reads and writes it on every call. Each wrapper you create gets its own private Map.

caller
heavyFee(1)
wrapper
cache.has(x)?
hit → return cache.get(x)
miss → call func
heavyFee(x)
heavy work
The wrapper sits between the caller and the original heavyFee. On a cache miss it calls through; on a hit it never reaches heavyFee at all.

From the outside, decorated heavyFee behaves exactly like the original — same inputs, same outputs. It just skips the heavy work when it already knows the answer.

Try it below. The wrapped heavyFee counts how many times it actually did the “heavy work”. Call it with a value you’ve used before and the counter stays put — that call came from the cache. Edit the code to change what heavyFee computes.

interactiveA caching decorator in action

Keeping the decorator separate from heavyFee buys you three things:

  • Reuse. The same cachingDecorator wraps any qualifying function, not just this one.
  • Clarity. heavyFee stays focused on its computation; the caching machinery never clutters it.
  • Composition. You can layer decorators — a caching wrapper around a logging wrapper around the original — and each stays independent.

Using func.call to pass the context

The decorator above has a blind spot: it breaks on object methods. Watch what happens when the wrapped function relies on this.

// we'll make engine.cost caching
let engine = {
  factor() {
    return 1;
  },

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

// 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( engine.cost(1) ); // the original method works

engine.cost = cachingDecorator(engine.cost); // now make it caching

alert( engine.cost(2) ); // Whoops! Error: Cannot read property 'factor' of undefined

Line (*) reads this.factor() and blows up. Why?

The wrapper calls the original as func(x) on line (**) — a plain function call, no object before the dot. In strict mode (which module and class code always is) a plain call sets this to undefined, so this.factor throws. The same thing happens if you detach a method and call it bare:

let func = engine.cost;
func(2); // this is undefined inside — same failure

So the wrapper forwards the argument but loses the receiver. engine.cost(2) should run with this = engine, yet inside the wrapper that connection is gone.

engine.cost(2)
this = engine

wrapper
this = engine
calls func(x)
func(x)
this = undefined
this.factor 💥
Calling engine.cost(2) sets this=engine in the wrapper — but a bare func(x) inside drops it to undefined.

The fix is a built-in method that lets you call a function while explicitly choosing its this: func.call.

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

It runs func with this set to context, and the rest of the arguments passed through in order. These two lines are nearly identical:

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

Both call func with 1, 2, 3. The second one also pins this to obj.

Here call runs the same function against two different objects:

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

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

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

One function, two different receivers — pick a this below and watch the same function body read a different name. Notice a plain greet() with no call throws, exactly like the bare func(x) inside the broken wrapper.

interactiveChoosing this with func.call

And with an argument alongside the context:

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

let user = { name: "Maya" };

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

Now drop call into the wrapper so the original method receives the right this:

let engine = {
  factor() {
    return 1;
  },

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

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;
  };
}

engine.cost = cachingDecorator(engine.cost); // now make it caching

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

Trace the this as it travels:

  1. After decoration, engine.cost is the wrapper function(x) { ... }.
  2. Calling engine.cost(2) invokes the wrapper with 2 as the argument and this = engine, because engine is the object before the dot.
  3. Inside, on a cache miss, func.call(this, x) forwards that same this (engine) and that same argument (2) into the original method. The chain holds.

Going multi-argument

The decorator still only handles one argument. Give it a method that takes two and the single-key cache falls apart.

let engine = {
  cost(base, surge) {
    return base + surge; // scary CPU-hogger is assumed
  }
};

// should remember same-argument calls
engine.cost = cachingDecorator(engine.cost);

For one argument x, cache.set(x, result) and cache.get(x) were enough. With two, the cache key has to represent the combination (base, surge). A Map accepts a single value as its key, so you need a way to fold several arguments into one.

Several approaches work:

  1. Build or import a multi-key map structure that keys on several values at once.
  2. Nest maps — cache.get(base) returns a Map keyed by surge, so a result lives at cache.get(base).get(surge).
  3. Fold the arguments into one value. Here a string like "base,surge" makes a fine key. To keep the decorator general, let the caller pass a hash function that knows how to squash many values into one.

Option 3 covers most real needs, so that’s the route.

You also have to forward all the arguments, not just the first. Inside a regular function the special arguments object holds every argument passed, so func.call(this, x) becomes func.call(this, ...arguments).

let engine = {
  cost(base, surge) {
    alert(`Called with ${base},${surge}`);
    return base + surge;
  }
};

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];
}

engine.cost = cachingDecorator(engine.cost, hash);

alert( engine.cost(4, 6) ); // works
alert( "Again " + engine.cost(4, 6) ); // same (cached)
arguments
(4, 6)
→ hash →
key
“4,6”
Map
“4,6” → 10
Two arguments collapse into one cache key via hash, then the whole argument list forwards through call.

The two edits:

  • Line (*) runs hash(arguments) to build one key. This simple join turns (4, 6) into "4,6". Trickier data would call for a smarter hash.
  • Line (**) uses func.call(this, ...arguments) to forward both the context and every argument the wrapper received — not just the first.

Feed two arguments to the decorated function below. The panel shows the hash key it built and whether the pair was already cached. Notice (4, 6) and (6, 4) are different keys — order matters to the hash.

interactiveCaching on a combination of arguments

The hash above is still hardwired to two arguments. Making it accept any number is the next problem, and it leads somewhere interesting.

func.apply

There’s a second way to write that forwarding line. Instead of func.call(this, ...arguments), you can use func.apply:

func.apply(context, args)

It runs func with this = context, taking args — an array-like object — as the full argument list. The only difference from call is how you hand over the arguments: call wants them spread out one by one, apply wants them bundled in an array-like.

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

Both make the same call. The distinction is narrow but real:

  • Spread (...) works on any iterable args, so call accepts iterables.
  • apply accepts only array-like args (something with a numeric length and indexed items).
call
func.call(ctx, a, b, c)
args spread as a list · any iterable
apply
func.apply(ctx, [a, b, c])
args as one array-like · array-like only
Same call, two shapes. call takes a comma list; apply takes one array-like bundle.

For something that’s both iterable and array-like — a real array — either works, and apply is often marginally faster because engines optimize it well.

Handing every argument plus the context straight to another function has a name: call forwarding. Its minimal form:

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

To outside code, calling wrapper is indistinguishable from calling func directly. It passes through whatever this and whatever arguments it got.

Borrowing a method

Back to that hash function stuck at two arguments:

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

It would be better if it glued together any number of values. arr.join does exactly that kind of gluing:

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

But this throws. The call site passes hash(arguments), and arguments is array-like and iterable, yet it is not a real array — so it has no join method:

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

hash(1, 2);

There’s a clean workaround:

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

hash(1, 2);

This is method borrowing. You grab join off an ordinary array literal ([].join) and run it via call with this set to arguments. The method executes as if it belonged to arguments.

The demo below calls a function with however many values you type. Inside, arguments has no join of its own, so the direct call fails — but the borrowed [].join.call(arguments, glue) succeeds. Change the glue string and re-run.

interactiveBorrowing Array.prototype.join for arguments
[ ]
has .join
.join.call(
arguments
array-like, no .join
) → “1,2”
arguments has no join of its own — so borrow one from a real array and run it with this = arguments.

Why does borrowing work at all? Because arr.join never assumes it’s running on a genuine array. Its internal algorithm, near-verbatim from the spec:

  1. Let glue be the first argument, or a comma "," if none was given.
  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. …continue until this.length items are joined.
  7. Return result.

It only ever reads this[0], this[1], … up to this.length. Any array-like this satisfies that, arguments included. Many built-in array methods are written this way on purpose — they’re generic, defined to work on array-likes, which is what makes borrowing possible.

Decorators and function properties

Swapping a function for a decorated version is usually harmless, with one catch. If the original function carried properties — say func.calledCount, or some flag you set on it — the wrapper won’t have them. The wrapper is a different function object, so heavyFee.calledCount disappears the moment heavyFee becomes cachingDecorator(heavyFee). If any code depends on those properties, decoration silently breaks it.

Decorators can also add properties of their own. A timing decorator might track how many times the function ran and how long it took, then expose those numbers as properties on the wrapper.

There is a way to decorate while preserving access to the original’s properties, but it needs a Proxy object to wrap the function transparently. That’s covered later in Proxy and Reflect.

Summary

A decorator is a wrapper around a function that changes its behavior while the original function still does the real work. Think of decorators as features you clip onto a function — one or many — without touching its source.

Building cachingDecorator walked through two forwarding methods:

Generic call forwarding is usually written with apply:

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

You also saw method borrowing: lifting a method off one object and running it, via call, against another. Applying array methods to arguments is the classic case; the modern substitute is a rest-parameter array (...args), which is a real array from the start.

Decorators show up everywhere in real codebases — debouncers, memoizers, loggers, retry wrappers. Work through this chapter’s tasks to lock the pattern in.