Hugo JS

Decorator in JavaScript

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.

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 I 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

Without decorator

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

With decorator

Try it out by yourself!

Conclusion

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

You can check the proposal out on GitHub.

Thank you for reading!

#javascript #react #node-js #programming #webdev

Decorator in JavaScript
4 Likes93.10 GEEK