What is a Decorator in JavaScript?
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.
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?
- When you call
fakeMedium.getWriter()
, its getter-mode is detected and runs the customizedget
method. - Inside the
get
method, thegetWriter
is newly bound bymedium
–this.getWriter.bind(medium)
. Here,this
refers tofakeMedium
itself. So it’s the same asfakeMedium.getWriter.bind(medium)
. That’s why itsget
is called once again. - But before the function is bound,
isDefining
is set to true, so the codes under the if-condition won’t be executed untilisDefining
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) {}
target
refers to either the class or a prototype of the class.value
isundefined
for a class and is the name of the method for a method.descriptor
is an object that contains definable properties on an object – such as configurable, writable, enumerable, and value. It’sundefined
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:
- medium.com