Decorator is just a standard in ECMAScript, JavaScript doesn’t implement this standard syntax, so we can’t use Decorator syntax directly.

But because Decorator is so nice, Babel provides plugins that allow us to use the Decorator syntax in javascript.

What is a Decorator?

A Decorator is a normal JavaScript function (pure function is recommended) used to change the members (properties, methods) of a class and the class itself. When you use the @decoratorFunction syntax on class members and class headers, the decoratorFunction is called by passing some arguments and can be used to modify the class and class members.

You need to see the syntax of property descriptor before understanding Decorator.

Get Start

Since JavaScript does not yet support Decorator syntax, we need to do some preparation work.

Install Babel or TypeScript and convert the code containing the Decorator syntax into code that the JavaScript engine will understand.

For simplicity’s sake, use Babel.

  1. Install @baebl/core and @babel/cli

    1
    2
    3
    4
    
    $ npm install --save-dev @babel/core @babel/cli
    
    $ npx babel --version
    7.10.4 (@babel/core 7.10.4)
    
  2. Install @babel/preset-env and @babel/plugin-proposal-decorators

    1
    2
    
    $ npm install --save-dev @babel/preset-env
    $ npm install --save-dev @babel/plugin-proposal-decorators
    

    @babel/preset-env contains some pre-defined standard babel plugins and configurations, and @babel/plugin-proposal-decorators is used for decorator syntax conversions.

  3. Add the babel.config.json file

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    {
        "presets": [
            "@babel/preset-env"
        ],
        "plugins": [
            [
                "@babel/plugin-proposal-decorators",
                {
                    "decoratorsBeforeExport": true
                }
            ]
        ]
    }
    
  4. Compile the file

    You can create a file decoratorTest.js containing the decorator syntax and then use the command npx babel decoratorTest.js -o decoratorTest.out.js to compile the file.

Class methods Decorator

Create the user.js file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class User {
    constructor(firstname, lastName) {
        this.firstname = firstname;
        this.lastName = lastName;
    }

    getFullName() {
        return this.firstname + " " + this.lastName;
    }
}

let user = new User("John", "Doe");

console.log(Object.getOwnPropertyDescriptor(
    User.prototype, "getFullName"
));

User.prototype.getFullName = function() {
    return "HACKED!";
}

console.log(user.getFullName());

Compile, output.

1
2
3
4
5
6
7
8
$ npx babel user.js -o user.out.js && node user.out.js
{
  value: [Function: getFullName],
  writable: true,
  enumerable: false,
  configurable: true
}
HACKED!

We can see that the writable property of the descriptor of getFullName is true, so we can change the value of this method as we like, so we end up with HACKED!.

To avoid the method being modified, you need to change the descriptor of getFullName .

Note: getFullName is located on User.prototype and the class method is the same as the class property, except that its value is a function.

Use Object.defineProperty to modify the descriptor.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class User {
    constructor(firstname, lastName) {
        this.firstname = firstname;
        this.lastName = lastName;
    }

    getFullName() {
        return this.firstname + " " + this.lastName;
    }
}

// 修改 writable 为 false
Object.defineProperty(User.prototype, "getFullName", {
    writable: false
});

let user = new User("John", "Doe");

console.log(Object.getOwnPropertyDescriptor(
    User.prototype, "getFullName"
));

Compiled output.

1
2
3
4
5
6
7
$ npx babel user.js -o user.out.js && node user.out.js
{
  value: [Function: getFullName],
  writable: false,
  enumerable: false,
  configurable: true
}

If you change the value of the getFullName function at this point, it will have no effect and, in strict mode, will simply report an error.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class User {
    constructor(firstname, lastName) {
        this.firstname = firstname;
        this.lastName = lastName;
    }

    getFullName() {
        return this.firstname + " " + this.lastName;
    }
}

// 修改 writable 属性
Object.defineProperty(User.prototype, "getFullName", {
    writable: false
});

let user = new User("John", "Doe");

console.log(Object.getOwnPropertyDescriptor(
    User.prototype, "getFullName"
));

// 修改 getFullName 函数值,报错!
User.prototype.getFullName = function() {
    return "HACKED!";
}

console.log(user.getFullName());

Compiled output.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
$ npx babel user.js -o user.out.js && node user.out.js
{
  value: [Function: getFullName],
  writable: false,
  enumerable: false,
  configurable: true
}
D:\study\decorator\user.out.js:34
User.prototype.getFullName = function () {
                           ^

TypeError: Cannot assign to read only property 'getFullName' of object '#<User>'
    at Object.<anonymous> (D:\study\decorator\user.out.js:34:28)
    at Module._compile (internal/modules/cjs/loader.js:1256:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1277:10)
    at Module.load (internal/modules/cjs/loader.js:1105:32)
    at Function.Module._load (internal/modules/cjs/loader.js:967:14)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
    at internal/main/run_main_module.js:17:47

If there were a lot of methods like getFullName that needed to do the same thing to avoid being modified by Hackers, the workload would get bigger and bigger, and that’s where decorator comes in. Let’s use decorator to achieve the same thing.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 定义 decorator 函数
function readonly(target) {
    target.descriptor.writable = false;
    return target;
}

class User {
    constructor(firstname, lastName) {
        this.firstname = firstname;
        this.lastName = lastName;
    }

    // 在需要添加装饰器的方法前,添加 decorator
    @readonly
    getFullName() {
        return this.firstname + " " + this.lastName;
    }
}

let user = new User("John", "Doe");

console.log(Object.getOwnPropertyDescriptor(
    User.prototype, "getFullName"
));

First define a decorator function and add @decoratorFunction at the top where you want to add decorations.

Compile and output the above code.

1
2
3
4
5
6
7
$ npx babel user.js -o user.out.js && node user.out.js
{
  value: [Function: getFullName],
  writable: false,
  enumerable: false,
  configurable: true
}

You can see that the writable of getFullName is false, and after defining the decorator function, we can reuse it using the @decoratorFunction syntax.

The argument of the decorator function, the target object, contains the description of the element (class method, class property and class itself) to be modified. The structure of the target object is as follows.

1
2
3
4
5
6
7
8
{
  kind: 'method' | 'accessor' | 'field' | 'class',
  key: '<property-name>',
  descriptor: <property-descriptor>,
  placement: 'prototype' | 'static' | 'own',
  initializer: <function>,
  ...
}

The kind attribute identifies the type of the element (the target to be modified), whether it is a method, a member of the class, or the class itself. key is the name of the element. You can get more information at decorators proposal. Another important property is descriptor, which contains the description of the element’s properties.

Let’s change the previous code to print the target object.

1
2
3
4
5
6
7
function readonly(target) {
    console.log(target);
    target.descriptor.writable = false;
    return target;
}

...

Compiled output.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Object [Descriptor] {
  kind: 'method',
  key: 'getFullName',
  placement: 'prototype',
  descriptor: {
    value: [Function: getFullName],
    writable: true,
    configurable: true,
    enumerable: false
  }
}

You can see the details of kind, key and placement, with placement indicating that the getFullName method is on the prototype of the class.

You can also pass arguments to the decorator function @decoratorFunc(.... .args). Since this is a decorator function call, the decorator function defined must return a function to decorate the element, and you can consider the decorator function defined at this point to be a higher-order function.

We use this form to define a change decorator to achieve the same effect as @readonly.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function change(key, value) {
    return function(target) {
        target.descriptor[key] = value;
        return target;
    };
}

class User {
    constructor(firstname, lastName) {
        this.firstname = firstname;
        this.lastName = lastName;
    }

    @change("writable", false)
    getFullName() {
        return this.firstname + " " + this.lastName;
    }
}

let user = new User("John", "Doe");

console.log(Object.getOwnPropertyDescriptor(
    User.prototype, "getFullName"
));

When the class method is static, the method is on the class itself, not on its prototype, see the following example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function change(key, value) {
    return function(target) {
        console.log("target", target);
        target.descriptor[key] = value;
        return target;
    };
}

class User {
    @change("writable", false)
    static getVersion() {
        return "1.0.0";
    }
}

// 注意 Object.getOwnPropertyDescriptor 的第一个参数是 User
console.log(Object.getOwnPropertyDescriptor(User, "getVersion"));

User.getVersion = function() {
    return "HACKED!";
};

console.log(User.getVersion());

Compiled output.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
$ npx babel user.js -o user.out.js && node user.out.js
target Object [Descriptor] {
  kind: 'method',
  key: 'getVersion',
  placement: 'static',
  descriptor: {
    value: [Function: getVersion],
    writable: true,
    configurable: true,
    enumerable: false
  }
}
{
  value: [Function: getVersion],
  writable: false,
  enumerable: false,
  configurable: true
}
D:\study\decorator\user.out.js:74
User.getVersion = function () {
                ^

TypeError: Cannot assign to read only property 'getVersion' of function 'function User() {
    _classCallCheck(this, User);

    _initialize(this);
  }'
    at Object.<anonymous> (D:\study\decorator\user.out.js:74:17)
    at Module._compile (internal/modules/cjs/loader.js:1256:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1277:10)
    at Module.load (internal/modules/cjs/loader.js:1105:32)
    at Function.Module._load (internal/modules/cjs/loader.js:967:14)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
    at internal/main/run_main_module.js:17:47

We can see that target.placement is static, which means the element is static, because we modify writalbe to false, so we get an error when we reassign getVersion.

Class Properties Decorator

Defines a User class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class User {
    firstName = "default-first-name";
    lastName = "default-last-name";

    getFullName = function() {
        return this.firstName + " " + this.lastName;
    }

    constructor(firstName, lastName) {
        if (firstName) this.firstName = firstName;
        if (lastName) this.lastName = lastName;
    }
}

var dummy = new User();
console.log("dummy =>", dummy);
console.log("dummy.getFullName() =>", dummy.getFullName());

var user = new User("John", "Doe");
console.log("user =>", user);
console.log("user.getFullName() =>", user.getFullName());

First compilation output.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ npx babel user.js -o user.out.js && node user.out.js
dummy => User {
  firstName: 'default-first-name',
  lastName: 'default-last-name',
  getFullName: [Function (anonymous)]
}
dummy.getFullName() => default-first-name default-last-name
user => User {
  firstName: 'John',
  lastName: 'Doe',
  getFullName: [Function (anonymous)]
}
user.getFullName() => John Doe

At this point, if you output User.prototype, you will see that it does not have firstName and lastName, or even getFullName, because you defined getFullName with the = syntax, and getFullName is a class property at this point, except that Its value is a function, and the class property is defined on the instance of the class. This means that if we want to decorate a class property, we need to do so when the instance is created.

Next, create a @upperCase decorator that will update the default value of the instance property.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function upperCase(target) {
    console.log("target", target);

    const value = target.initializer();

    target.initializer = function() {
        return value.toUpperCase();
    };

    return target;
}
class User {
    // style 1
    @upperCase
    firstName = "default-first-name";

    // stype 2
    @upperCase lastName = "default-last-name";

    getFullName = function() {
        return this.firstName + " " + this.lastName;
    }

    constructor(firstName, lastName) {
        if (firstName) this.firstName = firstName;
        if (lastName) this.lastName = lastName;
    }
}

var dummy = new User();
console.log("dummy.getFullName() =>", dummy.getFullName());

Compile to see the results.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
$ npx babel user.js -o user.out.js && node user.out.js
target Object [Descriptor] {
  kind: 'field',
  key: 'firstName',
  placement: 'own',
  descriptor: { configurable: true, writable: true, enumerable: true },
  initializer: [Function: value]
}
target Object [Descriptor] {
  kind: 'field',
  key: 'lastName',
  placement: 'own',
  descriptor: { configurable: true, writable: true, enumerable: true },
  initializer: [Function: value]
}
dummy.getFullName() => DEFAULT-FIRST-NAME DEFAULT-LAST-NAME

You can see target.kind: "field" and target.placement: "own", which means this is a class property.

target.initializer is a function whose return value is used to initialize the value of the class property, so we can modify the target.initializer function in the decorator function for the purpose of modifying the default value.

target.initializer is also applicable to static properties.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function upperCase(target) {
    console.log("target", target);

    const value = target.initializer();

    target.initializer = function() {
        return value.toUpperCase();
    };

    return target;
}
class User {
    // style 1
    @upperCase
    static firstName = "default-first-name";

    // stype 2
    @upperCase static lastName = "default-last-name";

    static getFullName = function() {
        return this.firstName + " " + this.lastName;
    }
}

console.log("getFullName() =>", User.getFullName());

Compiled output.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
$ npx babel user.js -o user.out.js && node user.out.js
target Object [Descriptor] {
  kind: 'field',
  key: 'firstName',
  placement: 'static',
  descriptor: { configurable: true, writable: true, enumerable: true },
  initializer: [Function: value]
}
target Object [Descriptor] {
  kind: 'field',
  key: 'lastName',
  placement: 'static',
  descriptor: { configurable: true, writable: true, enumerable: true },
  initializer: [Function: value]
}
getFullName() => DEFAULT-FIRST-NAME DEFAULT-LAST-NAME

Class Decorator

Decorators can also decorate the class itself. For example, you want to dynamically add a method to the class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class User {
    constructor(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}

User.getVersion = function() {
    return "1.0.0";
}

User.prototype.getFullName = function() {
    return this.firstName + " " + this.lastName;
}

console.log("version =>", User.getVersion());

let user = new User("John", "Doe");
console.log("full-name =>", user.getFullName());

The above code dynamically adds a static method getVersion and a non-static method getFullName to the class. We can use decorator to achieve the same effect by first printing the value of target.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function peek(target) {
    console.log("target", target);
    return target;
}

@peek
class User {
    constructor(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    static getVersion() {
        return "1.0.0";
    }
}

Compiled output.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ npx babel user.js -o user.out.js && node user.out.js
target Object [Descriptor] {
  kind: 'class',
  elements: [
    Object [Descriptor] {
      kind: 'method',
      key: 'getVersion',
      placement: 'static',
      descriptor: [Object]
    }
  ]
}

You can see that target is a little different, it has a kind attribute of class and contains an elements attribute, elements indicates the targets of the class (elements, targets that the decorator can decorate), and at this point it has a static method of getVersion.

What we want to do is to add a new target to the elements array and a non-static method element below it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function add({name, callback}) {
    return function(target) {
        target.elements.push({
            kind: "method",
            key: name,
            placement: "prototype",
            descriptor: {
                value: callback,
                writable: false,
                configurable: false,
                enumerable: false
            }
        });

        return target;
    }
}

@add({
    name: "getFullName",
    callback: function() {
        return this.firstName + " : " + this.lastName;
    }
})
class User {
    constructor(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    static getVersion() {
        return "1.0.0";
    }
}

let user = new User("John", "Doe");
console.log("full-name =>", user.getFullName());

Compiled output.

1
2
$ npx babel user.js -o user.out.js && node user.out.js
full-name => John : Doe

As you can see, with the decorator, we have successfully added a method like a class.