Legacy (Deprecated)
LEGACY: A minimal guide to JavaScript (ECMAScript) Decorators and Property Descriptor of the Object
A short introduction of ECMAScript “decorators” proposal with basic examples and a little bit about the ECMAScript ecosystem.
--
⚠️ This article has been revised recently due to the change in the ECMAScript decorators proposal. You can read the latest version of this article from here.
Why ECMAScript Decorators instead of JavaScript Decorators in the title? Because ECMAScript is a standard for writing scripting languages like JavaScript. It doesn’t enforce JavaScript to support all the specs but a JavaScript engine (implemented inside a browser) may or may not support a feature introduced in ECMAScript or support with little different behavior.
Consider ECMAScript as a language that you speak such as English. Then JavaScript would be a dialect like British English. A dialect is a language in itself but it is based on principals of the language it was derived from. So, ECMAScript is a cookbook for cooking/writing JavaScript and it’s up to the chef/developer to follow all ingredients/rules or not.
Generally, JavaScript adopters (such as web browsers or server-side platforms such as Node.js) implement all the specifications written in the language (else the whole point of having a standard crumbles) and usually ship it with a beta-preview version to make sure if the implementation is stable.
TC39 or Technical Committee 39 at ECMA International is responsible for maintaining the ECMAScript standard. Members of this team belong to ECMA International, browser vendors, and companies interested in the web industry in general such a Google, Mozilla, etc.
As ECMAScript is an open standard, anybody can suggest new ideas or features that would be a great addition to the language. Therefore, a proposal for a new feature goes through 4 main stages and TC39 gets involved in this process until that feature is approved and included to the standard.
+-------+-----------+----------------------------------------+
| stage | name | mission |
+-------+-----------+----------------------------------------+
| 0 | strawman | Present a new feature (proposal) |
| | | to TC39 committee. Generally presented |
| | | by TC39 member or TC39 contributor. |
+-------+-----------+----------------------------------------+
| 1 | proposal | Define use cases for the proposal, |
| | | dependencies, challenges, demos, |
| | | polyfills etc. A champion |
| | | (TC39 member) will be |
| | | responsible for this proposal. |
+-------+-----------+----------------------------------------+
| 2 | draft | This is the initial version of |
| | | the feature that will be |
| | | eventually added. Hence description |
| | | and syntax of feature should |
| | | be presented. A transpiler such as |
| | | Babel should support and |
| | | demonstrate implementation. |
+-------+-----------+----------------------------------------+
| 3 | candidate | Proposal is almost ready and some |
| | | changes can be made in response to |
| | | critical issues raised by adopters |
| | | and TC39 committee. |
+-------+-----------+----------------------------------------+
| 4 | finished | The proposal is ready to be |
| | | included in the standard. |
+-------+-----------+----------------------------------------+
Right now (as of June 2018), Decorators are in stage 2 and we have Babel plugin to transpile decorators babel-plugin-transform-decorators-legacy
. In stage 2, as the syntax of the feature is subjected to change, it’s not recommended to use it in production as of now. In any case, decorators are beautiful and very useful to achieve things quicker.
From here on, we are working on a JavaScript feature that is still considered experimental by JavaScript engines, so your Node.js version might not support it. Therefore, we need Babel or TypeScript transpiler to convert decorator syntax into vanilla JavaScript syntax. Use the js-plugin-starter plugin to set up a very basic project. I have configured this boilerplate to work well with what we are going to cover in this article.
To understand decorators, we need to first understand what is a property descriptor of a JavaScript object property. A property descriptor is a set of rules on an object property, like whether a property is writable or enumerable. When we create a simple object and add some properties to it, each property has a default property descriptor.
var myObj = {
myPropOne: 1,
myPropTwo: 2
};
myObj
is a simple JavaScript object which looks like below in the console.
Now, if we write a new value to myPropOne
property like below, the operation will be successful and we will get the changed value.
myObj.myPropOne = 10;
console.log( myObj.myPropOne ); //==> 10
To get the property descriptor of property, we need to use Object.getOwnPropertyDescriptor(obj, propName)
method. Own here means return the property descriptor of propName
property only if that property belongs to the object obj
and not on its prototype chain.
let descriptor = Object.getOwnPropertyDescriptor(
myObj,
'myPropOne'
);console.log( descriptor );
Object.getOwnPropertyDescriptor
method returns an object with keys describing the permissions and current state of the property. value
is the current value of the property, writable
is whether the user can assign a new value to the property, enumerable
is whether this property will show up in enumerations like for in
loop or for of
loop or Object.keys
etc.
configurable
is whether the user has permission to change property descriptor and make changes to writable
and enumerable
. Property descriptor also has get
and set
keys which are middleware functions to return value or update value, but these are optional.
To create a new property on an object or update existing property with a custom descriptor, we use Object.defineProperty
. Let’s modify an existing property myPropOne
with writable
set to false
, which should disable writes to myObj.myPropOne
.
'use strict';var myObj = {
myPropOne: 1,
myPropTwo: 2
};// modify property descriptor
Object.defineProperty( myObj, 'myPropOne', {
writable: false
} );// print property descriptor
let descriptor = Object.getOwnPropertyDescriptor(
myObj, 'myPropOne'
);
console.log( descriptor );// set new value
myObj.myPropOne = 2;
As you can see from the above error, our property myPropOne
is not writable, hence if a user is trying to assign a new value to it, it will throw an error.
If
Object.defineProperty
is updating existing property descriptor, then original descriptor will be merged with new modifications.Object.defineProperty
returns the original objectmyObj
after changes.
Let’s see what will happen if we set enumerable
descriptor key to false
.
var myObj = {
myPropOne: 1,
myPropTwo: 2
};// modify property descriptor
Object.defineProperty( myObj, 'myPropOne', {
enumerable: false
} );// print property descriptor
let descriptor = Object.getOwnPropertyDescriptor(
myObj, 'myPropOne'
);
console.log( descriptor );// print keys
console.log(
Object.keys( myObj )
);
As you can see from the above result, we can’t see myPropOne
property of the object in Object.keys
enumeration.
When you define a new property on object using Object.defineProperty
and pass empty {}
descriptor, the default descriptor looks like below.
Now, let’s define a new property with custom descriptor where configurable
descriptor key is set to false
. We will keep writable
to false
and enumerable
to true
with value
set to 3
.
var myObj = {
myPropOne: 1,
myPropTwo: 2
};// modify property descriptor
Object.defineProperty( myObj, 'myPropThree', {
value: 3,
writable: false,
configurable: false,
enumerable: true
} );// print property descriptor
let descriptor = Object.getOwnPropertyDescriptor(
myObj, 'myPropThree'
);
console.log( descriptor );// change property descriptor
Object.defineProperty( myObj, 'myPropThree', {
writable: true
} );
By setting configurable
descriptor key to false
, we lost the ability to change descriptor of our property myPropThree
. This is very helpful if you don’t want your users to manipulate the recommended behavior of an object.
get (getter) and set (setter) for a property can also be set in property descriptor. But when you define a getter, it comes with some sacrifices. You can not have an initial value or value
key on the descriptor at all because the getter will return the value of that property. You can not use writable
key on descriptor as well, because your writes are done through the setter and you can prevent writes there. Have a look at MDN documentation of getter and setter, or read this article because they don’t need much explanation here.
You can create and/or update multiple properties at once using
Object.defineProperties
which takes two arguments. First argument is target object on which properties has to be added/modified and second argument is object withkey
as property name andvalue
as its property descriptor. This function returns the target object.
Have you tried Object.create
function to create objects? This is the easiest way to create an Object with no or a custom prototype. It is also one of the easier ways to create objects from scratch with custom property descriptors.
Object.create
function has the following syntax.
var obj = Object.create( prototype, { property: descriptor, ... } )
Here prototype
is an object which will be the prototype of the obj
. If prototype
is null
, then obj
won’t have any prototype. When you define an empty or non-empty object with var obj= {}
syntax, by default, obj.__proto__
points to Object.prototype
hence obj
has the prototype of Object
class.
This is similar to using Object.create
with Object.prototype
as the first argument (prototype of the object being created).
'use strict';var o = Object.create( Object.prototype, {
a: { value: 1, writable: false },
b: { value: 2, writable: true }
} );console.log( o.__proto__ );
console.log(
'o.hasOwnProperty( "a" ) => ', o.hasOwnProperty( "a" )
);
But when we set the prototype to null
, we get below error.
'use strict';var o = Object.create( null, {
a: { value: 1, writable: false },
b: { value: 2, writable: true }
} );console.log( o.__proto__ );
console.log(
'o.hasOwnProperty( "a" ) => ', o.hasOwnProperty( "a" )
);
✱ Class Method Decorator
Now that we understood how we can define and configure new or existing properties of an object, let’s move our attention to decorators and why we discussed property descriptors at all.
A decorator is a JavaScript function (recommended pure function) that is used to modify class properties/methods or class itself. When you add @decoratorFunction
syntax on the top of a class property, method or class itself, decoratorFunction
gets called with few arguments which we can use to modify class or class properties.
Let’s create a simple readonly
decorator function. But before that, let’s create simple User
class with getFullName
method which returns the full name of the user by combining firstName
and lastName
.
class User {
constructor( firstname, lastName ) {
this.firstname = firstname;
this.lastName = lastName;
}getFullName() {
return this.firstname + ' ' + this.lastName;
}
}// create instance
let user = new User( 'John', 'Doe' );
console.log( user.getFullName() ); // John Doe
Above code prints John Doe
to the console. But there is a huge problem, anybody can modify getFullName
method.
User.prototype.getFullName = function() {
return 'HACKED!';
}
With this, now we get the below result.
HACKED!
To avoid public access to override any of our methods, we need to modify property descriptor of getFullName
method which lives on User.prototype
object.
Object.defineProperty( User.prototype, 'getFullName', {
writable: false
} );
Now, if any user is trying to override getFullName
method, he/she will get below error.
But if we have many methods on the User
class, doing this manually won’t be so great. This is where decorator comes in. We can achieve the same thing by putting @readonly
annotation on top of getFullName
method as below.
function readonly( target, property, descriptor ) {
descriptor.writable = false;
return descriptor;
}class User {
constructor( firstname, lastName ) {
this.firstname = firstname;
this.lastName = lastName;
}@readonly
getFullName() {
return this.firstname + ' ' + this.lastName;
}
}User.prototype.getFullName = function() {
return 'HACKED!';
}
Have a look at readonly
method. It accepts three arguments. property
is name of the property/method which belongs to target
object (which is same as User.prototype
) and descriptor
is the property descriptor for that property. From within a decorator function, we have to return the descriptor
at any cost. This descriptor
will replace the existing property descriptor of that property.
There is another version of decorator syntax which goes like @decoratorWrapperFunction( ...customArgs )
. But with this syntax, decoratorWrapperFunction
should return a decoratorFunction
which is the same as used in the previous example.
function log( logMessage ) {
// return decorator function
return function ( target, property, descriptor ) {
// save original value, which is method (function)
let originalMethod = descriptor.value;// replace method implementation
descriptor.value = function( ...args ) {
console.log( '[LOG]', logMessage );// here, call original method
// `this` points to the instance
return originalMethod.call( this, ...args );
};return descriptor;
}
}class User {
constructor( firstname, lastName ) {
this.firstname = firstname;
this.lastName = lastName;
}@log('calling getFullName method on User class')
getFullName() {
return this.firstname + ' ' + this.lastName;
}
}var user = new User( 'John', 'Doe' );
console.log( user.getFullName() );
Decorators do not differentiate between static
and non-static
methods. The below code will work just fine, the only thing that will change is how you access the method. The same applies to Instance Field Decorators which we will see next.
@log('calling getVersion static method of User class')
static getVersion() {
return 'v1.0.0';
}console.log( User.getVersion() );
✱ Class Instance Field Decorator
So far, we have seen changing property descriptor of a method with @decorator
or @decorator(..args)
syntax, but what about public/private properties (class instance fields)?
Unlike typescript
or java
, JavaScript classes do not have class instance fields AKA class properties. This is because anything defined in the class
and outside the constructor
should belong to the class prototype. But there is a new proposal to enable class instance fields with public
and private
access modifiers, which is now in stage 3, and we have babel transformer plugin for it.
Let’s define a simple User
class but this time, we don’t need to set default values for firstName
and lastName
from within the constructor.
class User {
firstName = 'default_first_name';
lastName = 'default_last_name';constructor( firstName, lastName ) {
if( firstName ) this.firstName = firstName;
if( lastName ) this.lastName = lastName;
}getFullName() {
return this.firstName + ' ' + this.lastName;
}
}var defaultUser = new User();
console.log( '[defaultUser] ==> ', defaultUser );
console.log( '[defaultUser.getFullName] ==> ', defaultUser.getFullName() );var user = new User( 'John', 'Doe' );
console.log( '[user] ==> ', user );
console.log( '[user.getFullName] ==> ', user.getFullName() );
Now, if you check prototype
of User
class, you won’t be able to see firstName
and lastName
properties.
Class instance fields are a very helpful and important part of Object-Oriented Programming (OOP). It’s good that we have a proposal for that but the story is far from over.
Unlike class methods that live on the class prototype, class instance fields live on object/instance. Since the class instance field is neither part of the class nor its prototype, it’s a little tricky to manipulate its descriptor. Babel gives us initializer
function on the property descriptor of the class instance field instead of value
key. Why initializer
function instead of value
, this topic is in debate and since Decorators
are in stage-2, no final draft has been published to outline this but you can follow this answer on Stack Overflow to understand the background story.
That being said, let’s modify our early example and create a simple @upperCase
decorator that will change the case of the class instance field’s default value.
function upperCase( target, name, descriptor ) {
let initValue = descriptor.initializer();descriptor.initializer = function(){
return initValue.toUpperCase();
}return descriptor;
}class User {
@upperCase
firstName = 'default_first_name';
lastName = 'default_last_name';constructor( firstName, lastName ) {
if( firstName ) this.firstName = firstName;
if( lastName ) this.lastName = lastName;
}getFullName() {
return this.firstName + ' ' + this.lastName;
}
}console.log( new User() );
We can also make use of the decorator function with parameters to make it more customizable.
function toCase( CASE = 'lower' ) {
return function ( target, name, descriptor ) {
let initValue = descriptor.initializer();
descriptor.initializer = function(){
return ( CASE == 'lower' ) ?
initValue.toLowerCase() : initValue.toUpperCase();
}
return descriptor;
}
}class User {
@toCase( 'upper' )
firstName = 'default_first_name';
lastName = 'default_last_name';constructor( firstName, lastName ) {
if( firstName ) this.firstName = firstName;
if( lastName ) this.lastName = lastName;
}getFullName() {
return this.firstName + ' ' + this.lastName;
}
}console.log( new User() );
descriptor.initializer
function is used internally by Babel to create value
of property descriptor of an object property. This function returns the initial value assigned to the class instance field. Inside the decorator, we need to return another initializer
function that returns the final value.
Class instance field proposal is highly experimental and there is a strong chance that it’s syntax might change until it goes to stage-4. Hence, it’s not a good practice to use class instance fields with decorators yet.
✱ Class Decorator
Now we are familiar with what decorators can do. They can change the properties and behavior of class methods and class instance fields, giving us the flexibility to dynamically achieve those things with a simpler syntax.
Class decorators are a little bit different than decorators we saw earlier. Previously, we used property descriptors to modify the behavior of a property or method, but in the case of a class decorator, we need to return a constructor function.
Let’s understand what a constructor function is. Underneath, a JavaScript class is nothing but a function that is used to add prototype methods and define some initial values for the fields.
function User( firstName, lastName ) {
this.firstName = firstName;
this.lastName = lastName;
}User.prototype.getFullName = function() {
return this.firstName + ' ' + this.lastName;
}let user = new User( 'John', 'Doe' );console.log( user );
console.log( user.__proto__ );
console.log( user.getFullName() );
Here is a great article to understand
this
in JavaScript.
So when we call new User
, User
function is invoked with arguments we passed and in return, we got an object. Hence, User
is a constructor function. BTW, every function in JavaScript is a constructor function, because if you check function.prototype
, you will get constructor
property. As long as we are using new
keyword with a function, we should expect an object in return.
If you
return
a valid JavaScript Object from constructor function, then that value will be used instead of creating new object usingthis
assignments. That will break the prototype chain though because retuned object won’t have any prototype methods of constructor function.
With that in mind, let’s focus on what a class decorator can do. A class decorator has to be on the top of the class, like previously we have seen decorator on method name or field name. This decorator is also a function but it should return a constructor function instead of a class.
Let’s say I have a simple User
class like below.
class User {
constructor( firstName, lastName ) {
this.firstName = firstName;
this.lastName = lastName;
}
}
Our User
class do not have any method at the moment. As discussed, a class decorator must return a constructor function.
function withLoginStatus( UserRef ) {
return function( firstName, lastName ) {
this.firstName = firstName;
this.lastName = lastName;
this.loggedIn = false;
}
}@withLoginStatus
class User {
constructor( firstName, lastName ) {
this.firstName = firstName;
this.lastName = lastName;
}
}let user = new User( 'John', 'Doe' );
console.log( user );
A class decorator function will receive the target class UserRef
, which is User
in the above example (on which decorator is applied) and must return a constructor function. This opens the door of infinite possibilities that you can do with the decorator. Hence class decorators are more popular than method/property decorators.
But the above example is too basic and we wouldn’t want to create a new constructor when our User
class might have tons of properties and prototype methods. Good thing is, we have reference to the class from within the decorator function i.e. UserRef
. We can return a new class from constructor function and that class will extends User
class (more accurately UserRef
class). Since the class is also a constructor function underneath, this is legal.
function withLoginStatus( UserRef ) {
return class extends UserRef {
constructor( ...args ) {
super( ...args );
this.isLoggedIn = false;
}setLoggedIn() {
this.isLoggedIn = true;
}
}
}@withLoginStatus
class User {
constructor( firstName, lastName ) {
this.firstName = firstName;
this.lastName = lastName;
}
}let user = new User( 'John', 'Doe' );
console.log( 'Before ===> ', user );// set logged in
user.setLoggedIn();
console.log( 'After ===> ', user );
You can chain multiple decorators together by placing them on top of each other. The order of execution will be the same as the order of their appearance.
Decorators are a fancy way to achieve things faster. Wait for some time until they are added to ECMAScript specifications.