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.
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?
fakeMedium.getWriter()
, its getter-mode is detected and runs the customized get
method.get
method, the getWriter
is newly bound by medium
— this.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.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?
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
is undefined
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’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?
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!
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