🎄 Get 20% off from our JavaScript course for the holidays! 🎄
The Decorator Design Pattern in JavaScript

The Decorator Design Pattern in JavaScript

Benchmark and optimize your code with the help of decorators
Ferenc AlmasiLast updated 2021 November 11 • Read time 7 min read
Learn how you can use decorators in JavaScript, to wrap a method or a property into another function to extends its original functionality...
  • twitter
  • facebook
JavaScript

If you were working before with Angular or Mobx, you already somewhat familiar with decorators as they make heavy use of them. They have been part of other languages for long, but they are coming to JavaScript as well.

We can already use them today with polyfillers or compilers such Babel. If you are not familiar with decorators, we are talking about the following:

observable decorators in JavaScript

Here @observable is a decorator, which is responsible for wrapping the two properties with additional functionality. So how are they created?

Let’s first take a step back and define what design patterns are in general.


What Are Design Patterns?

Design patterns are tested solutions to common problems in software development. The different patterns can be categorized into three different categories: creational, structural, and behavioral.

I already wrote about a commonly used behavioral design pattern in JavaScript, the PubSub pattern. Now is the time to discuss a structural pattern used for adding enhanced functionality to objects dynamically.


The Concept of the Decorator Pattern

We already saw how decorators look like. They are denoted with an @ symbol followed by the name of the decorator. Decorators are really just syntactic sugar for higher-order functions. They essentially wrap a method or a property into another function that extends its original functionality.

It can be anything as simple as adding logs or as complex as defining access for user groups. It’s important to mention that you can only use decorators for properties or methods, this is why you will see objects used throughout this tutorial.

Looking to improve your skills? Check out our interactive course to master JavaScript from start to finish.
Master JavaScript Remove ads

Creating Decorators

To work with decorators, you’ll need to first set up Babel with a decorators plugin. It will transpile your code down to ES6 so browsers will be able to parse it correctly. If you only want to experiment with decorators but don’t want to configure anything, you can use Repl on the official site of Babel.

decorators mode set to legacy in babel
Don’t forget to set the decorators mode to legacy if using Repl

So what are decorators? — Decorators are really just functions which takes three parameters as arguments:

  • object: the object on which we want to define the property
  • property: the name of the property or method on the object which we want to modify
  • descriptor: describes how the new property or method should behave

If you ever used Object.defineProperty you may recognize some similarities between the two. Let’s see a very simple example:

Copied to clipboard! Playground
const greetPatrick = (object, property, descriptor) => {
    descriptor.value = () => {
    	console.log('Hello Patrick!');
    }
}

const spongeBob = {
    @greetPatrick
    greet() {
    	console.log('Hello!')
    }
}

spongeBob.greet();
greet.js

We have an object with a greet method and we want to override the body of the method. We annotated the function with a decorator. To create that decorator, you simply have to create a function with the same name which takes the three parameters we discussed. Assigning descriptor.value to a new function overrides the original and we get back Hello Patrick! in the console.

console logging decorated function call

Now let’s see two more practical examples: one for testing performance and another one for improving it.

@time decorator

Say we have a function to get the first n of triangular numbers and we want to know the time it takes to generate them:

Copied to clipboard! Playground
const calculate = {
  
    triangularNumbers(n) {
        const nums = [];

        for (let i = 0; i < n; i++) {
            nums.push((i * (i + 1)) / 2);
        }

        return nums;
    }
};
triangularNumbers.js

We have a simple function with a loop and we want to decorate it. So we create a new decorator called @time:

Copied to clipboard! Playground
const time = (object, property, descriptor) => {
    const originalFunction = descriptor.value;

    descriptor.value = (...args) => {
        console.time(`Time it takes to run ${property}`);
        const originalValue = originalFunction(...args);
        console.timeEnd(`Time it takes to run ${property}`);
    
        return originalValue;
    }
}
time.js

This time, we don’t want to override the whole body of the function, rather we want to extend it with additional functionality. So we start off by storing the original function in descriptor.value.

We can use destructuring to get all available properties passed into the original function and get the value of the function return, which we then store in originalValue. This is what we return at the end. We wrap everything in console.time which measures the time it takes for the function to run.

If we use the decorator and call the function we get back the following printed to the console:

using the time decorator

Now imagine this is done with the first 1,000,000 sets of numbers. Time increases so we want to optimize this function with another decorator:

@memoize decorator

If you haven’t heard about memoization, it’s all about caching previous results. So instead of rerunning calculations for previous executions, we can instead return the cached value.

So again, we create another function called memoize:

Copied to clipboard! Playground
const memoize = (object, property, descriptor) => {
    const originalFunction = descriptor.value;
    const cache = {};

    descriptor.value = (...args) => {
        const cachedValue = cache[args.toString()];

        if (!cachedValue) {
            cache[args.toString()] = originalFunction(...args);
        }

        return cachedValue ? cachedValue : originalFunction(...args);
    }
}
memoize.js

In this function, we can create a cache object to store previous values. Then we can either get or assign a new value to a property named based on the passed parameters to the function. This will ensure that we have unique properties and only one value will be assigned to one set of results.

If we already have a cached value, we return that. Otherwise, we execute the original function.

Now we need to assign both decorators to the calculate function:

Copied to clipboard! Playground
const calculate = {
  
    @time
    @memoize
    triangularNumbers(n) {
        const nums = [];
        
        for (let i = 0; i < n; i++) {
            nums.push((i * (i + 1)) / 2)
        }

        return nums;
    }
};
calculate.js

First, we want to call @time then @memoize. If we run the method again two times we see that for the first time, it takes about 100ms to run, but in the second time, it only takes ~0.014ms as the result is requested from the cache.

time and memoize decorators used together
This is roughly a 99.98% decrease in time.

Summary

Of course, the list goes on. You can add extra properties to objects or use it for more complex situations such as Angular’s @Component or Mobx’s @observable or @observer.

Since we are talking about function calls, you can also use decorators by passing values into them:

Copied to clipboard! Playground
@Component({
    selector: 'selector',
    template: `
      Component template...
    `
})
class BaseComponent {
    ...
}
component.js
Just as Angular does for the @Component decorator

What are your favourite use-cases of decorators? Have you used them before? Let us know in the comments below. Thank you for reading through, Happy coding!

Did you find this page helpful?
📚 More Webtips
Frontend Course Dashboard
Master the Art of Frontend
  • check Access exclusive interactive lessons
  • check Unlimited access to hundreds of tutorials
  • check Remove ads to learn without distractions
Become a Pro

Recommended