Decorator,中文译作装饰器、修饰器,Decorator 只是 ECMAScript 中的一个标准,JavaScript 并未实现这个标准语法,所以我们无法直接使用 Decorator 的语法。
但是由于 Decorator 十分 nice,所以 Babel 提供了插件,可以让我们在 javascript 中使用 Decorator 语法。
Decorator 就是一个用来改变类的成员(属性、方法)和类本身的普通的 JavaScript 函数(建议使用纯函数)。当你在类成员和类的头部使用 @decoratorFunction
语法时,decoratorFunction
会被传递一些参数调用,可以用来修改类和类成员。
理解 Decorator 之前需要看一下 property descriptor 的语法。
因为 JavaScript 还不支持 Decorator 语法,所以我们需要做一些准备工作。
安装 Babel 或者 TypeScript,将包含 Decorator 语法的代码转换成 JavaScript 引擎能够理解的代码。
为了简单起见,使用 Babel。
1、安装 @baebl/core
和 @babel/cli
$ npm install --save-dev @babel/core @babel/cli
$ npx babel --version
7.10.4 (@babel/core 7.10.4)
2、安装 @babel/preset-env
和 @babel/plugin-proposal-decorators
$ npm install --save-dev @babel/preset-env
$ npm install --save-dev @babel/plugin-proposal-decorators
@babel/preset-env
包含了一些预设的标准的 babel 插件和配置,@babel/plugin-proposal-decorators
用于 decorator 语法转换。
3、添加 babel.config.json
文件
{
"presets": [
"@babel/preset-env"
],
"plugins": [
[
"@babel/plugin-proposal-decorators",
{
"decoratorsBeforeExport": true
}
]
]
}
4、编译文件
至此,你已经完成了编译 decorator 语法的基本环境配置了,你可以创建一个包含 decorator 语法的文件 decoratorTest.js
,然后使用 npx babel decoratorTest.js -o decoratorTest.out.js
命令编译文件。
创建 user.js 文件:
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());
编译,输出结果:
$ npx babel user.js -o user.out.js && node user.out.js
{
value: [Function: getFullName],
writable: true,
enumerable: false,
configurable: true
}
HACKED!
可以发现 getFullName
的 descriptor 的 writable
属性时 true
,所以我们可以随意更改这个方法的值,因此最后输出了 HACKED!
。
为了避免方法被修改,需要更改 getFullName
的 descriptor。
注意:
getFullName
位于User.prototype
上,类方法与类属性一样,只不过它的值是函数。
使用 Object.defineProperty
修改 descriptor。
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"
));
编译输出结果:
$ npx babel user.js -o user.out.js && node user.out.js
{
value: [Function: getFullName],
writable: false,
enumerable: false,
configurable: true
}
如果此时,修改 getFullName
函数的值,将不会有任何作用,并且严格模式下,会直接报错。
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());
编译输出结果:
$ 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
如果有很多方法与 getFullName
一样,都需要做同样的处理来避免被 Hacker 修改,那么工作量会越来越大,这也正是 decorator 的作用。下面我们用 decorator 来实现同样的作用。
// 定义 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"
));
首先定义了一个 decorator 函数,并在需要添加装饰的顶部添加 @decoratorFunction
。
编译输出上面的代码。
$ npx babel user.js -o user.out.js && node user.out.js
{
value: [Function: getFullName],
writable: false,
enumerable: false,
configurable: true
}
可以看到 getFullName
的 writable
为 false
,定义了 decorator 函数后,我们可以使用 @decoratorFunction
语法重复使用。
decorator 函数的参数 target
对象,包含了要修改的 element(类方法、类属性和类本身)的描述信息。target
对象的结构如下:
{
kind: 'method' | 'accessor' | 'field' | 'class',
key: '<property-name>',
descriptor: <property-descriptor>,
placement: 'prototype' | 'static' | 'own',
initializer: <function>,
...
}
kind
属性标识 element(要修改的目标)的类型,它是类的方法、成员或者是类本身。key
是 element 的名称。你可以在 decorators proposal 获得更多的信息。另外还有一个比较重要的属性是 descriptor
,它包含了 element 的属性描述。
我们更改之前的代码,打印 target
对象。
function readonly(target) {
console.log(target);
target.descriptor.writable = false;
return target;
}
...
编译输出结果:
Object [Descriptor] {
kind: 'method',
key: 'getFullName',
placement: 'prototype',
descriptor: {
value: [Function: getFullName],
writable: true,
configurable: true,
enumerable: false
}
}
可以看到 kind
、key
和 placement
的详细信息,placement
表示 getFullName
这个方法在类的 prototype 上。
你还可以给 decorator 函数传递参数 @decoratorFunc(...args)
。因为这是一个 decorator 函数调用,所以定义的 decorator 函数必须返回一个函数用来装饰 element,你可以认为此时定义的 decorator 函数是一个高阶函数。
我们使用这种形式,来定义一个 change
装饰器,来达到 @readonly
同样的效果。
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"
));
当类方法是 static 时,这个方法是在 class 本身,而不是它的 prototype
上,看下面的例子。
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());
编译输出结果:
$ 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
可以看到 target.placement
为 static
表示 element 是 static 的,因为我们修改 writalbe
为 false
,所以再重新赋值 getVersion
时报错。
定义一个 User
类。
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());
首次编译输出结果:
$ 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
此时,如果你输出 User.prototype
,你会发现它没有 firstName
和 lastName
,甚至没有 getFullName
,因为你定义 getFullName
,是通过 =
语法定义的,此时的 getFullName
是一个类属性,只不过它的值是函数,类的属性是定义在类的 instance 上的。这意味着,如果我们想装饰类的属性,我们需要在 instance 被创建时装饰。
接下来,创建一个 @upperCase
装饰器,用来更新 instance 属性的默认值。
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());
编译查看结果:
$ 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
可以看到 target.kind: "field"
、target.placement: "own"
,表示这是一个类属性。
target.initializer
是一个函数,函数的返回值用来初始化类属性的值,所以我们可以在装饰器函数中修改 target.initializer
函数以达到修改默认值的目的。
target.initializer
同样适用于 static properties
。
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());
编译输出:
$ 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
Decorators 同样可以装饰类本身。例如,你想动态的给类增加一个方法。
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());
上面的代码中,动态的给类添加了一个 static 方法 getVersion
和一个非 static 方法 getFullName
。我们可以使用 decorator 实现同样的效果,首先打印查看一下 target
的值。
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";
}
}
编译输出:
$ 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]
}
]
}
可以看到 target
有一点不同,它的 kind
属性为 class
,并且包含了 elements
属性,elements
指示了这个类中的 targets(elements, 装饰器可以装饰的目标),此时他有一个 getVersion
的静态方法。
我们想要做的是在 elements
数组中添加一个新的 target,下面添加一个 非 static 方法的 element。
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());
编译输出结果:
$ npx babel user.js -o user.out.js && node user.out.js
full-name => John : Doe
可以看到,通过装饰器,我们成功的在类里面添加了一个方法。
上面介绍的是目前新的 Decorator 语法,关于旧的语法可以看下面这篇文章。
Legacy: A minimal guide to JavaScript (ECMAScript) Decorators and Property Descriptor of the
Object
(完)