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.
-
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)
|
-
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.
-
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
}
]
]
}
|
-
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.