现在的前端项目中,装饰器作为装饰器模式的语法糖得到了越来越广泛的应用。本文会先介绍装饰器的具体用法,然后会将 Javascript 中的装饰器和其他语言常见的装饰器用法进行比较,展示 Javascript 中装饰器的不同之处。

什么是 Decorator

Decorator 装饰器是 ECMAScript 的一个语言功能提案,目前(文章书写时间 2022-02-09 13:54:26)还处于 stage-2 阶段。但是借助 TypeScript 或者 Babel,已经有大量的优秀开源项目深度使用上它了。比如:VS Code, Angular, Nest.js, Mobx 等等。Decorator 装饰器本质上是一种特殊的函数,在定义类、类元素时通过 @ 来引用该函数,可以对被装饰的目标进行注入添加新的功能而无需从根本上改变其外部行为。

一个 Decorator 的例子:

1
2
3
4
@defineElement("my-class")
class C extends HTMLElement {
@reactive accessor clicked = false;
}

Decorator 的作用

主要的功能

Decorator 有四个主要的功能

  1. 它可以用具有相同语义的匹配值替换正在修饰的值。(例如,装饰器可以用另一个方法替换一个方法,用另一个字段替换一个字段,用另一个类替换一个类,等等)。
  2. 它可以将元数据与正在修饰的值相关联。然后可以从外部读取这些元数据,并将其用于元编程(metaprogramming, 编写能改变语言语法特性或者运行时特性的程序)和自省(introspection, 检查对象的一种能力)。
  3. 它可以通过元数据提供对正在修饰的值的访问。对于公共值(public),他们可以通过值的名称来实现这一点。对于私有值(private),它们接收访问器函数(accessor functions),然后可以选择共享这些函数。
  4. 它可以初始化被修饰的值,在值被完全定义后运行额外的代码。如果该值是类的成员,则每个实例都会进行一次初始化。

生命周期

  1. 装饰器表达式(在 @ 符号后的内容)的求值过程中穿插着计算属性名。
  2. 在类定义期间,在对方法进行评估之后,但在将构造函数和原型组合在一起之前,会调用装饰器(作为函数)。
  3. 在调用了所有装饰器之后,装饰器会同时被应用(修改构造函数和原型)。

应用场景

常见应用场景:

日志记录,性能统计,类型检查

Decorator的局限性

使用装饰器语法特性的确可以带来很多方便,但还是有许多需要注意的地方,一方面是由于 Javascript 语言实现上的特性,一方面是装饰器语法本身的问题,还有一方面是语法方案本身进度的问题。

不支持方法装饰器

有关方法装饰器可参见 function-decorators-and-annotations 。其他语言像 python 是支持方法装饰器的。但是因为 Javascript 中的方法会变量提升,而装饰器并不会提升(详见Why decorator should not be hoisted),所以 Javascript Decorator 草案初版不打算支持方法装饰器。详见Allow decorating functions

不支持编译时装饰器

像 java 之类语言,装饰器(java一般叫注解 Annotation)可以在编译时产生不影响输出的代码。但 Javascript 不存在输出字节码这步,原生当然不支持编译时装饰器。不过工具如 Typescript 其实理论上是可以支持的。

无法被tree-shaking

目前从实现上来说,无论是 babel-plugin-transform-decorators 还是 Typescript 本质上都是一个 expression 。构建工具都没办法知道这个 expression 做了什么,所以被装饰的类没有办法被 tree-shaking 。例如下面的 TestClass 就无法被 tree-shaking 。

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
class TestClass {
@Foo()
private content = '';
}

// es6
class TestClass {
constructor() {
this.content = '';
}
}
__decorate([
Foo(),
__metadata("design:type", Object)
], TestClass.prototype, "content", void 0);

// es5
var TestClass = /** @class */(function () {
function TestClass() {
this.content = '';
}
__decorate([
Foo(),
__metadata("design:type", Object)
], TestClass.prototype, "content", void 0);
return TestClass;
}());

代码压缩工具如 Terser 或 Uglify.js 支持用 /*@__PURE__*/comment来声明方法为 pure 。

1
const x = /*#__PURE__*/i_am_dropped_if_x_is_not_used()

Typescript 中可以使用 /** @class */ 注释。

1
2
3
4
var TestClass = /** @class */(function () {
//...
return TestClass;
}());

我们可以在构建工具利用字符替换将这个注释替换成自动替换成 pure 。

1
2
3
4
5
6
7
8
9
plugins: [
resolve({ jsnext: true, module: true }),
{
transform(code, id) {
return code.replace(/\/\*\* @class \\*\//g, "\/*@__PURE__*\/");
}
},
uglify(uglify_options),
]

过多的装饰器增加调试成本

装饰器层次增多,会增加调试成本,很难追溯到一个 Bug 是在哪一层包装导致的。

装饰器语法仍处于草案状态

当前的 tc39 装饰器语法草案 与 Babel “legacy” decorators 和 Typescript “experimental” decorators 实现均不同。虽然 Babel 7 已经支持了 TC39 2018.11 会议上通过的草案,但其实现仍旧面临了性能上的问题。所以 tc39 的建议是,如果在之前已经使用了装饰器语法,那么继续按照老的方案使用。

Typescript 中的 Decorator 实现

目前 Typescript 中的 Decorator 实现是基于 2014 年的 JavaScript 装饰器语法草案。详细实现参见 Typescript “experimental” decorators

Decorator 例子

假设我们要实现一个 Person 的类,这个类包括两个方法 walkrun,我们通过装饰器记录 walkrun 消耗的时间。

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 logTime(target, key, desc) {
const fn = desc.value;
const logTime = function (...args) {
const start = Date.now();
try {
return fn.apply(this, args);
} finally {
const end = Date.now();
console.log(`${key} time: ${end - start}ms`);
}
}
desc.value = logTime;
return desc;
}

class Person {

@logTime
walk() {
console.log('Walk');
}

@logTime
run() {
console.log('Run');
}
}

const ming = new Person();
ming.walk();
ming.run();

__decorate 方法解析

上述代码经过 Typescript 转换后变为如下代码(emitDecoratorMetadata=false 不生成装饰器元数据):

可以在 Typescript Playground 看到完整代码

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
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length,
r = c < 3 ? target:
desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc,
d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
r = Reflect.decorate(decorators, target, key, desc);
else
for (var i = decorators.length - 1; i >= 0; i--)
if (d = decorators[i])
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
function logTime(target, key, descriptor) {/* ... */}
class Persion {/* ... */}
__decorate([
logTime
], Person.prototype, "walk", null);
__decorate([
logTime
], Person.prototype, "run", null);
const ming = new Person();
ming.walk();
ming.run();

上述代码有关装饰器实现的核心点是两个部分,一个是定义,一个是执行。

  • Part1 装饰器工具函数 __decorate 的执行

    执行会传入以下 4 个参数:

    1. 装饰器业务逻辑函数
    2. 类的构造器
    3. 类的构造器属性名
    4. 属性描述符(可以为null)
  • Part2 装饰器工具函数 __decorate 的定义

    首先,将该函数主要逻辑无关的部分剥离

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    var __decorate = function (decorators, target, key, desc) {
    // Step1 备份原来类构造器 (Class.prototype) 的属性描述符 (Descriptor)
    var r = desc = Object.getOwnPropertyDescriptor(target, key),
    var d;

    for (var i = decorators.length - 1; i >= 0; i--) {
    // d 为装饰器业务逻辑函数
    if (d = decorators[i]) {
    // Step2 执行 d,并传入 target 类构造器,key 属性名,r 属性描述符
    r = d(target, key, r) || r;
    }
    }

    // Step3 用装饰器函数覆盖原来属性描述符
    Object.defineProperty(target, key, r);
    return r;
    };

    我们可以看到,Typescript 对装饰器编译后的代码,是将装饰器逻辑抽离成了一个工具函数,这个工具函数主要包括三个部分:

    1. 使用 Object.getOwnPropertyDescriptor 备份原来的类构造器的属性描述符
    2. 执行装饰器业务逻辑函数,并传入类构造器、属性名、属性描述符
    3. 使用 Object.defineProperty 将装饰器函数覆盖原来的属性描述符

Decorator 类型

Typescript 中的装饰器一共有5种类型:

  1. 类装饰器(Class)
  2. 类属性装饰器(Class Field / Property)
  3. 类方法装饰器(Class Method)
  4. 类访问器装饰器(Calss Accessor)
  5. 参数装饰器(Parameter)

需要注意的是,在最新版 tc39 stage2 草案中,参数装饰器暂时没有被安排在内。并且 tc39 的草案与 Typescript 的实现有较大差别。

Typescript 中各种装饰器的入参和返回值可参见以下描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface TypedPropertyDescriptor<T> {
enumerable?: boolean;
configurable?: boolean;
writable?: boolean;
value?: T;
get?: () => T;
set?: (value: T) => void;
}
// 类装饰器
declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
// 属性装饰器
declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;
// 方法装饰器/访问器装饰器
declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
// 参数装饰器
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;

Decorator 执行顺序

同一个类的方法有多个装饰器,其执行顺序为先从外到内进入,然后由内向外执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function dec(id) {
console.log('evaluation', id);
return function (target, key, desc) {
console.log('call', id);
return desc;
}
}
class Example {
@dec(1)
@dec(2)
method() {}
}
// evaluation 1
// evaluation 2
// call 2
// call 1

同一个类有不同类型的装饰器时,按照以下顺序执行:

  1. 按照 实例 >> 静态 >> 构造函数 > 类 的优先级顺序执行
  2. 同一优先级内的(参数|方法/访问器/属性)装饰器按照出现的先后顺序执行
  3. 同一个方法既有方法装饰器又有参数装饰器时,参数装饰器先执行
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
38
39
40
function d(value: string) {
console.log(value, 'evaluation');
return function(target: any, key?: any, desc?: any) {
console.log(value, 'call');
}
}

@d('Class')
class C {
@d('Static Property')
static prop: any;

@d('Instance Property')
prop: any;

constructor(@d('Constructor Parameter') foo: void) {}

@d('Static Method')
static method(@d('Static Method Parameter') foo: void){}

@d('Instance Method')
method(@d('Instance Method Parameter') foo: void){}
}

// Instance Property evaluation
// Instance Property call
// Instance Method evaluation
// Instance Method Parameter evaluation
// Instance Method Parameter call
// Instance Method call
// Static Property evaluation
// Static Property call
// Static Method evaluation
// Static Method Parameter evaluation
// Static Method Parameter call
// Static Method call
// Class evaluation
// Constructor Parameter evaluation
// Constructor Parameter call
// Class call