A brief introduction to some basic examples of the "decorator" proposal in JavaScript and ECMAScript related content

Why use ECMAScript decorators instead of JavaScript decorators in titles? Because ECMAScript is the standard for writing scripting languages such as JavaScript, it does not force JavaScript to support all specifications, but JavaScript engines (used by different browsers) may or may not support features introduced by ECMAScript, Or support some different behavior.

Think of ECMAScript as some language you speak, such as English. So JavaScript is like British English. A dialect is a language in itself, but it is based on the principles of the language from which it comes. As such, ECMAScript is a “cookbook” for cooking/writing JavaScript, and it’s up to the chef/developer to follow or not follow all the ingredients/rules.

In general, JavaScript adopters follow all the specifications written in the language (otherwise developers will be driven crazy) and release a new version of the JavaScript engine after it appears and until they are sure everything works. ECMA International’s TC39 or Technical Committee 39 is responsible for maintaining the ECMAScript language specification. Typically, the team consists of ECMA International, browser vendors, and companies interested in the web.

Because ECMAScript is an open standard, anyone can come up with new ideas or features and push them forward. Thus, a proposal for a new feature goes through four main stages, and TC39 is involved in the process until the feature is ready for implementation.

phase The name of the task
0 strawman Propose new features (suggestions) to TC39 Committee. Generally provided by TC39 members or TC39 contributors.
1 proposal Define proposals, dependencies, challenges, examples, polyfills, and other use cases. A champion (TC39 member) will be responsible for this proposal.
2 draft This is the draft version of the final version. Therefore, you need to provide a description and syntax for this functionality. In addition, syntax compilers like Babel need to be supported.
3 candidate The proposal is ready with some amendments to address key issues raised by adopters and the TC39 Committee.
4 finished The proposal is ready to be incorporated into the specification

Until now (June 2018), decorator was in phase 2, and we made a Babel plugin, babel-plugin-Transformer-decorators-Legacy, to transform decorator functionality. In phase 2, the syntax of the functionality may change, so it is not recommended to use this functionality in current production projects. In any case, I find decorators elegant and effective in achieving quick goals.

From now on, we’re experimenting with experimental JavaScript, so your version of Node.js may not support these features. So we need a syntax compiler like Babel or TypeScript. Using the JS-plugin-starter plugin to create a very basic project, I added something to support this article.


To understand decorators, we need to first understand what a property descriptor is for a JavaScript object property. Property descriptor is a set of rules for an object property, such as whether the property is writable or enumerable. When we create a simple object and add some properties, each property has a default property descriptor.

var myObj = {
    myPropOne: 1.myPropTwo: 2
};
Copy the code

MyObj is a simple JavaScript object as shown in the console below.

Now, if we write a new value to the myPropOne property below, the operation will succeed and we will get the changed value.

myObj.myPropOne = 10;
console.log( myObj.myPropOne ); / / = = > 10
Copy the code

For attribute property descriptor, we need to use the Object. The getOwnPropertyDescriptor (obj, propName) method. Here Own means that the property descriptor of the propName property is returned only if the property belongs to the object obj and not to the stereotype chain.

let descriptor = Object.getOwnPropertyDescriptor(
    myObj,
    'myPropOne'
);
console.log( descriptor );
Copy the code

Object. GetOwnPropertyDescriptor method returns a description attribute permissions and the current state of the key Object. Value is the current value of the property, writable is whether the user can assign a new value to the property, enumerable is whether the property shows up in enumerations like for in loops or for of loops or Object.keys. The writable and Enumerable components can be modified without varying the user’s property descriptor. Property Descriptors also have get and set middleware functions to return values or update keys to values, but these are optional.

To create a new property on an Object or update an existing property with a custom descriptor, we use Object.defineProperty. Let’s modify an existing property, myPropOne, with the writable property set to false, which will prohibit writing 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;
Copy the code

As you can see from the error above, our property myPropOne is not writable, so it will throw an error if the user tries to assign a new value to it.

If Object.defineProperty is updating an existing property Descriptor, the original descriptor will be overwritten by the new modification. After the change, Object.defineProperty returns the original Object myObj.

Let’s see what happens if Enumerable is set 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 )
);
Copy the code

As you can see, the myPropOne property is missing from the enumeration of Object.keys.

When you define a new property of an Object with Object.defineProperty, pass an empty {}descriptor, and the default descriptor will look something like this.

Now, let’s define a new property with a custom descriptor, which is different set to false, writable remains false, Enumerable is true, and valu is 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});Copy the code

By setting the 64x to false, we lose the ability to change the descriptor of the property myPropThree. This is useful if you don’t want the user to manipulate the default behavior of the object.

Get (getter) and set (setter) properties can also be set in property descriptor. But when you define a getter, it causes some loss. You can’t have an initial value or a value key on descriptor because the getter returns the value of that property. You can’t use the writable property on descriptor either, because your writes are done through setters, where you can block writes. Take a look at the MDN documentation for getters and setters, or read this article.

You can use Object.defineProperties with two parameters to create and/or update multiple properties at once. The first argument is the object to which the property is added/modified, and the second argument is the object whose property name is the key and whose value is property descriptor. This function returns the first target object.

Have you ever tried the object. create function to create an Object? This is the easiest way to create objects that have no or custom stereotypes. It’s also one of the easier ways to create objects from scratch using custom property descriptors. Here is the syntax for the object. create function.

var obj = Object.create( prototype, { property: descriptor, ... })Copy the code

Prototype here is the object that will be the prototype for OBJ. If the stereotype is null, obJ will have no stereotype. When defining a null or non-null Object with var obj = {}, by default, obj.__ proto__ refers to Object.prototype, so obj has a prototype of the Object class.

This is similar to using Object.create with Object.prototype as the first argument (the 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"));Copy the code

But when we set the stereotype to NULL, we get the following 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"));Copy the code


Now that we know how to define and configure new or existing properties of objects, let’s turn our attention to decorators, and why we talked about property Descriptors.

A Decorator is a JavaScript function (pure function recommended) that modifies class attributes/methods or the class itself. When you add the @DecoratorFunction syntax at the top of a class property, method, or class itself, the decoratorFunction is called with parameters that we can use to modify the class or class properties. Let’s create a simple Readonly decorator function. But before we do that, let’s create a simple User class using the getFullName method, which returns the User’s full name 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() );
Copy the code

The code above prints John Doe to the console. But the big problem is that anyone can modify the getFullName method.

User.prototype.getFullName = function() {
    return 'HACKED! ';
}
Copy the code

So now we have the following result.

HACKED!
Copy the code

To avoid public access overwriting any of our methods, we need to modify the property descriptor of the getFullName method on the User.prototype object.

Object.defineProperty( User.prototype, 'getFullName', {
    writable: false});Copy the code

Now, if any user tries to override the getFullName method, they will get the following error.

However, if we have a lot of methods in the User class, performing these operations manually will not be as good. That’s where decorators come in. We can do the same thing by placing the @readonly syntax at the top of the getFullName method 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! ';
}
Copy the code

Take a look at the readonly method. It takes three arguments. Property is the name of the property/method that belongs to the target object (same as user. prototype), and descriptor is the property descriptor of that property. From the decorator function, we have to return descriptor at all costs. The descriptor here is going to replace the existing property descriptor for that property.

There’s another version of the decorator syntax, like @decoratorWrapperFunction(… CustomArgs). But in this syntax, the decoratorWrapperFunction should return the same decoratorFunction 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 ); };returndescriptor; }}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() );
Copy the code

Decorators do not distinguish between static and non-static methods. The following code executes fine; the only thing that changes is how you access the method. The same applies to Instance Field Decorators as we’ll see below.

@log('calling getVersion static method of User class')
static getVersion() {
    return 'v1.0.0';
}
console.log( User.getVersion() );
Copy the code

Class Instance Field Decorator

So far we have seen the use of @decorator or @decorator(.. Args) syntax changes the property descriptor of a method, but what about public/private attributes (class instance fields)? Unlike typescript or Java, JavaScript classes do not have class instance field class attributes as we know them. This is because anything defined outside of a class and constructor should belong to a class stereotype. But there is a new scenario that uses public and private access modifiers to enable class instance fields, which is now in phase 3, and we have the corresponding Babel converter plug-in. Let’s define a simple User class, but this time we don’t need to set default values for firstName and lastName in 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() );
Copy the code

Now, if you examine the prototype of the User class, you will not see the firstName and lastName attributes.

Class instance fields are a useful and important part of object-oriented programming (OOP). It’s great that we have a proposal like this, but “the revolution is not done yet” folks.

Unlike class methods that are located on class prototypes, class instance fields are located on objects/instances. Since the class instance field is neither part of the class nor its prototype, it is not easy to manipulate its descriptor. Babel gives us the initialization function on the property descriptor of the class instance field, not the value key. Why you initialize functions instead of values is a subject of debate, as the decorator is in phase 2 and no final draft has been released to outline this, but you can follow this answer on Stack Overflow to understand the whole backstory.

Having said that, let’s modify our earlier example and create a simple @upperCase decorator that will change the case of the default value of the class instance field.

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() );
Copy the code

We can also use decorator functions and 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(); }returndescriptor; }}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() );
Copy the code

Initializer function used internally by Babel to create the property descriptor value of the object properties. This function returns the initial value assigned to the field of the class instance. Inside the decorator, we need to return another initialization function that returns the final value.

The class instance field proposal is highly experimental, and it is likely that its syntax will change until it reaches phase 4. Therefore, it is not a good practice to use class instance fields with decorators.

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 implement them dynamically using simpler syntax.

The class decorator is slightly different from the decorators we’ve seen before. Previously, we used property descriptors to modify the behavior of properties or methods, but in the case of class decorators, we need to return a constructor.

Let’s see what a constructor is. Below, the JavaScript class is nothing more than a function that adds a prototype method and defines 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() );
Copy the code

Here’s a great article to understand this in JavaScript.

So when we call new User, the User function is called with the argument we passed, and we get an object as a result. Therefore, User is a constructor. By the way, every function in JavaScript is a constructor, because if you check function.prototype, you’ll get constructor properties. As long as we use the new keyword in a function, we should expect an object to return the result.

If a valid JavaScript object is returned from the constructor, that value is used instead of having this allocate the new object created. This breaks the stereotype chain because the retuned object will not have any stereotype methods of the constructor. With that in mind, let’s focus on what class decorators can do. The class decorator must be at the top of the class, just as we saw on the method name or field name before. This decorator is also a function, but it should return a constructor or a class.

Suppose I have a simple User class, as follows.

class User {
    constructor( firstName, lastName ) {
        this.firstName = firstName;
        this.lastName = lastName; }}Copy the code

Our User class does not currently have any methods. As mentioned earlier, the class decorator must return a constructor.

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 );
Copy the code

The class decorator function will receive the target class UserRef, which is User from User (where the decorator is applied) in the example above, and must return a constructor. This opens the door to endless possibilities for decorators. So class decorators are more popular than method/property decorators.

The above example is fairly basic, and we don’t want to create a new constructor when our User class might have a large number of attributes and stereotype methods. Better yet, we can refer to the class in the decorator function, UserRef. We can return a new class from the constructor, and this class will extend the User class (more specifically, the UserRef class). Therefore, a class is also a constructor, which 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 );
Copy the code

You can chain multiple decorators by placing one decorator on top of another. The order of execution corresponds to the order in which they appear.

Decorators are a clever way to achieve your goals faster. They will be added to the ECMAScript specification in the near future.

A minimal guide to ECMAScript Decorators.


IVWEB Technology Weekly shocked online, pay attention to the public number: IVWEB community, weekly timing push quality articles.

  • Collection of weekly articles: weekly
  • Team open source project: Feflow