Technology Blog

Look deep into latest news and innovations happening in the Tech industry with our highly informational blog.

What is a Decorator in JavaScript?

hkis

When you write a class in JavaScript, you might have had to add more features to the methods in a class. But sometimes they look quite nasty and messy.

How can you make the process more elegant? In this post, we will talk about a promising feature, a decorator.

js_decorator

Before Reading

This feature isn’t included in the newest ECMA-262, JavaScript in other words. You should always use Babel to use this in your project.

The examples I’ve attached to this post were written in JSFiddle, with the Babel + JSX configuration. If you want to use this feature in your project, you ought to set up Babel on your own.

Without a Decorator

class Medium {
  constructor(writer) {
    this.writer = writer;
  }

  getWriter() {
    return this.writer;
  }
}

There’s a class, Medium, that takes the name of the writer in its constructor. And there’s a function that returns the writer’s name.

Let’s create a property that is of Medium type.

const medium = new Medium('Jane');
const fakeMedium = {
  writer: 'Fake Jane',
  getWriter: medium.getWriter,
};

medium is created using Medium‘s constructor function, unlike fakeMedium which is an object literal. But it has the same properties as medium.

Now, let’s compare the result of getWriter from each.

medium.getWriter(); // Jane
fakeMedium.getWriter(); // Fake Jane

Why are the values different?

It’s because JavaScript’s normal function this is bound to the object that actually invokes the function.

medium.getWriter() is called by the medium object, however, fakeMedium.getWriter() is called by fakeMedium. So, the this inside the function, getWriter, looks up the value from fakeMedium.

This article outlines the difference between normal functions and arrow functions.

To get the same result as when medium.getWriter is called, let’s use Object.defineProperty. What Object.defineProperty does is define new properties on the object or modify the existing properties on the object and then it returns the object.

const fakeMedium = { ... };
let isDefining;
let fn = fakeMedium.getWriter;
Object.defineProperty(fakeMedium, 'getWriter', {
  get() {
    console.log('Access to getWriter');
    if (isDefining) {
      return fn;
    }
    isDefining = true;
    const boundFn = this.getWriter.bind(medium);
    isDefining = false;
    
    return boundFn;
  }
});

Whenever fakeMedium.getWriter is called, Access to getWriter will be printed twice. But why twice?

  1. When you call fakeMedium.getWriter(), its getter-mode is detected and runs the customized get method.
  2. Inside the get method, the getWriter is newly bound by mediumthis.getWriter.bind(medium). Here, this refers to fakeMedium itself. So it’s the same as fakeMedium.getWriter.bind(medium). That’s why its get is called once again.
  3. But before the function is bound, isDefining is set to true, so the codes under the if-condition won’t be executed until isDefining is set back to false again.

But this way is really a pain in the neck. Because every time you make a new instance of Medium, you should do this again.

Can’t we do this in a more elegant way?

With a Decorator

Any function can be a decorator. Basically, you can use a decorator for either a class or a method in a class. It takes three arguments – target, value, and descriptor.

function decorator(target, value, descriptor) {}
  1. target refers to either the class or a prototype of the class.
  2. value is undefined for a class and is the name of the method for a method.
  3. descriptor is an object that contains definable properties on an object – such as configurable, writable, enumerable, and value. It’s undefined for a class.
function autobind(target, value, descriptor) {}
class Medium {
  ...
  @autobind
  getWriter() {
    return this.writer;
  }
}

A decorator is used with an at sign (@), with the name of the function that you’ll use as a decorator — and it takes three arguments as we just explained.

function autobind(target, value, descriptor) {
  const fn = descriptor.value;
  
  return {
    configurable: true,
    get() {
      return fn.bind(this);
    }
  }
}

descriptor.value is the name of the function on which you put the decorator function – in this case, it’s getWriter itself.

Note that the return value of autobind is a new object, then getWriter adopts the return value to its environment.

What’s good about using decorators is that they are reusable. All you need to do after defining the decorator function is merely to write @autobind on functions.

Here’s another example of making class member properties read-only, which is even easier.

function readonly(target, value, descriptor) {
  descriptor.writable = false;
  return descriptor;
}
class Medium {
  @readonly
  signUpDate = '2019-04-23';
}
const medium = new Medium();
medium.signUpDate; // 2019-04-23
medium.signUpDate = '1999-11-11'; 
medium.signUpDate; // 2019-04-23
^ The value isn't changed!

This time, the descriptor of the property has been changed by setting the writable property as false and that is all. Dead simple. Right?

Full Code Comparison

Here’s the comparison of the full code.

class Medium {
  constructor(writer) {
    this.writer = writer;
  }

  getWriter() {
    console.log(this.writer);
  }
}

const medium = new Medium('Jane');
const fakeMedium = {
  writer: 'Fake Jane',
  getWriter: medium.getWriter,
};

medium.getWriter(); // Jane
fakeMedium.getWriter(); // Fake Jane

/* Do auto-binding job for the same values */
let isDefining;
let fn = fakeMedium.getWriter;
Object.defineProperty(fakeMedium, 'getWriter', {
  get() {
    if (isDefining) {
      return fn;
    }
    isDefining = true;
    const boundFn = this.getWriter.bind(medium);
    isDefining = false;
    
    return boundFn;
  }
});

medium.getWriter(); // Jane
fakeMedium.getWriter(); // Jane
function autobind(target, value, descriptor) {
  const fn = descriptor.value;
  
  return {
    configurable: true,
    get() {
      return fn.bind(this);
    }
  }
}

class Medium {
  constructor(writer) {
    this.writer = writer;
  }
  
  @autobind
  getWriter() {
    console.log(this.writer);
  }
}

const medium = new Medium('Jane');
const fakeMedium = {
  writer: 'Fake Jane',
  getWriter: medium.getWriter,
};

medium.getWriter(); // Jane
fakeMedium.getWriter(); // Jane

Try it out by yourself!

Conclusion

A decorator is very useful, powerful, amazing, and remarkable. Honestly, we don’t see any reason to say no to use this awesome feature.

But, remember that it’s still at stage 2 and the way we used this in this post is more like Babel’s style, not the currently proposed one at stage 2. So, things might be different, like how to use it or what you can actually do with it.

So, we absolutely recommend you use this feature with the appropriate Babel configurations for your project but we also want to mention to keep an eye on this feature in TC39.

For more information and to develop your web app using front-end technology, Hire Front-End Developer from us as we give you a high-quality solution by utilizing all the latest tools and advanced technology. E-mail us any clock at – hello@hkinfosoft.com or Skype us: “hkinfosoft“. To develop your custom website using JS, please visit our technology page.

Content Source:

  1. medium.com