Design Patterns - Decorating in JavaScript


Today we are going to talk about the decorator design pattern. More specifically, how to decorate functions in JavaScript.

TL; DR

Source code can be found on GitHub, jsDecorator or jsFiddle


Design Patterns

Decorating in JavaScript

The decorator design pattern is for extending the functionality or state of an object by repeatedly wrapping it.

That's quite a broad definition, it can mean many different things. In JavaScript - and other languages with first class functions, like Python - there is a specific use case for this. Namely, to extend a function by wrapping it in an outer function.

This can be useful for temporarily augment functions while developing or debugging an application (logging, call counting, performance profiling, etc). But even production code can be greatly simplified and made more readable by adding frequently used extra functionality via decorators (authentication, caching, permission handling, analytics, etc).

Let's say we have a suspected bottleneck function:

function isPrime(num) {  
    if (num === 2 || num === 3) { 
        return true; 
    }
    if (num % 2 === 0) { 
        return false; 
    }
    let max = Math.ceil(Math.sqrt(num));
    for (let i = 3; i <= max; i += 2) {
        if (num % i === 0) {
            return false;
        }
    }
    return true;
}

We want to measure the time it takes to execute this function, but we don't really want to manually modify it's code, and revert it after the measurement. Not to mention this is a fairly common thing we do, so an ideal solution would be a write once - reuse any time. We can achieve that by a decorator function.

function logDecorator(func) {  
    let decorated = function () {
        let start = performance.now(),
            result = func.apply(this, arguments),
            time = Math.round((performance.now() - start) * 1000);
        console.log(`${time} μs`);
        return result;
    };  
    return decorated;
}

As you can see, our decorator function gets a func function argument, creates and returns a decorated replacement function, which does the extra functionality we want (logging time in this case), and calls the original func function, much like we used to call super in overridden methods.

We can apply our decorator to any function like this:

let isPrimeLog = logDecorator(isPrime);  
isPrimeLog(22586101147);    

// output:
// 4555 μs

I think it's easier to understand if we give it a separate name, also we will compare two differently decorated functions later, and in that case, they need different names anyway. But of course we could just as easily decorate the isPrime function in place:

isPrime = logDecorator(isPrime);  

By adding two more things, we can make our log decorator more generic and useful:

  1. The anonymous decorated function should be a named one. I generally try not to use anonymous functions unless it's justifiable, because they lead to unreadable stack traces. Also, I would like to log the function name. So I derive a name from the original name of func.
  2. I compile a string from the arguments, so I can log that too.

These functionalities will also be useful in our second example, so I've put them in separate utility functions (which I omitted for brevity - see the source code for details).

function logDecorator(func) {  
    let decorated = function () {
        let params = formatArguments(arguments),
            start = performance.now(),
            result = func.apply(this, arguments),
            time = Math.round((performance.now() - start) * 1000);
        console.log(`${decorated.name}(${params}) => ${result} (${time} μs)`);
        return result;
    };  
    return renameFunction(decorated, func.name + 'Log');
}

// output for the same call:
// isPrimeLog(22586101147) => true (4555 μs)

As you can see, the output became more rich and useful. And we can put this on any function we want. Now it's probably easier for you to imagine the things we can do with this. And we aren't limited to adding functionality, we can also add state via closure.

Consider the second example: we want to add caching to functions. If we call our function with a set of arguments, it calculates the result and stores it. Whenever we call it with the same set of arguments again, it will get the result from the cache instead of recalculating it every time. This is called memoization, and can be a massive performance increase in many situations. And if we have an easily applicable solution, we can try it on any function without messing with our code too much.

function memoDecorator(func) {  
    let cache = {},
        decorated = function () {
            let params = formatArguments(arguments);
            if (params in cache) {
                return cache[params];
            } else {
                let result = func.apply(this, arguments);
                cache[params] = result;
                return result;
            }
        };
    return renameFunction(decorated, func.name + 'Memo');
}

Let's try it out:

let num = 22586101147;

let isPrimeLog = logDecorator(isPrime);  
isPrimeLog(num);  
isPrimeLog(num);  
isPrimeLog(num);

// output
// isPrimeLog(22586101147) => true (4555 μs)
// isPrimeLog(22586101147) => true (5645 μs)
// isPrimeLog(22586101147) => true (3925 μs)

isPrimeMemoLog = logDecorator(memoDecorator(isPrime));  
isPrimeMemoLog(num);  
isPrimeMemoLog(num);  
isPrimeMemoLog(num);

// output
// isPrimeMemoLog(22586101147) => true (3485 μs)
// isPrimeMemoLog(22586101147) => true (5 μs)
// isPrimeMemoLog(22586101147) => true (5 μs)

Mostly we did the same thing as in the first example. One key difference to note is how we create the cache object in our decorator, so the decorated function can access it via closure. In other words, we gave a shared state to the different calls of the decorated function.

Also note how we double-decorated our second function.

As you can see memoDecorator makes subsequent calls return the cached result instantly.

When you wrap your head around it, using decorators in JavaScript is a simple, but very powerful design pattern. It can be used effectively and elegantly, but - as almost everything - can be abused too. I encourage you to try it, play with it, try to come up with use cases in your line of work, and see what you can build with it.

Finally, I want to mention that there are plans to include a special syntax for using decorators in ES7. You can read more about that here.


If you enjoyed reading this blog post, please share it or like the Facebook page of the website to get regular updates. You can also follow us @dealwithjs.

Happy decorating!