装饰器

状态: 初稿

介绍

随着 TypeScript 和 ES6 对类的引入, 现在开始出现一些要求能注解或修改类和类成员的额外特性的场景.
装饰器提供一种途径, 为类声明及其成员添加注解和元编程语法.
装饰器是一个 JavaScript 第二阶段提议, 以及 TypeScript 试验特性.

注  装饰器作为试验特性, 可能在将来版本中发生改变.

要启用对装饰器的试验性支持, 你必须在命令行或 tsconfig.json 文件启用 experimentalDecorators 编译器选项.

命令行:

1
tsc --target ES5 --experimentalDecorators

tsconfig.json 文件

1
2
3
4
5
6
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}

装饰器

装饰器是可以附加在类声明, 方法, 存取方法, 属性, 或参数上的一类特殊声明.
装饰器满足 @expression 形式, 其中, expression 必须当成函数求值, 在运行期, 求出的函数被调用, 其参数是关于被装饰声明的信息.

例如, 给定装饰器 @sealed, 我们可能会编写如下 sealed 函数:

1
2
3
function sealed(target) {
// do something with 'target' ...
}

注  你可以在后文类装饰器一节看到装饰器更详细的实例.

装饰器工厂

如果我们想自定义一个装饰器如何附加到一声明上, 可以编写装饰器工厂.
装饰器工厂只是一个函数, 它返回一个在运行期由装饰器调用的表达式.

我们可以参考以下样式编写装饰器工厂:

1
2
3
4
5
function color(value: string) { // this is the decorator factory
return function (target) { // this is the decorator
// do something with 'target' and 'value'...
}
}

注  你可以在后文方法装饰器一节看到装饰器工厂更详细的实例.

装饰器复合

多个装饰器可一并附加到同一个声明上, 如下例所示:

  • 置于单行:
1
@f @g x
  • 置于多行:
1
2
3
@f
@g
x

当多个装饰器附加于单一声明, 它们的求值过程十分类似数学中的复合函数. 在这个例子中, 我们复合函数 fg, 结果 (fg)(x) 等同于 f(g(x)).

由此, TypeScript 执行以下步骤求值附加在单一声明的多个装饰器:

  1. 从上往下求出每个装饰器的表达式.
  2. 从下往上调用求出的函数.

如果我们会用装饰器工厂, 可以通过下例观察求值顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function f() {
console.log("f(): evaluated");
return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("f(): called");
}
}

function g() {
console.log("g(): evaluated");
return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("g(): called");
}
}

class C {
@f()
@g()
method() {}
}

它会在终端打印下列输出:

1
2
3
4
f(): evaluated
g(): evaluated
g(): called
f(): called

对装饰器求值

附加到类中各种声明上的装饰器的应用顺序已经预先定义:

  1. 附加在每个实例成员上的参数装饰器, 然后是方法, 存取方法, 或属性装饰器.
  2. 附加在每个静态成员上的参数装饰器, 然后是方法, 存取方法, 或属性装饰器.
  3. 附加在构造器上的参数装饰器.
  4. 附加在类上的类装饰器.

类装饰器

类装饰器附加在一个类声明之前.
类装饰器作用于类的构造器, 可以监视, 修改, 或替换类定义.
类装饰器不能出现在声明文件, 或任何其他外部上下文环境中(比如以 declare 声明的类).

类装饰器的表达式作为函数在运行期被调用, 目标类的构造器是它唯一的参数.

如果类装饰器返回一个值, 它会用给定构造器函数替换类声明.

注  如果你选择返回新构造器函数, 你必须维护好类的旧原型.
运行期应用装饰器的逻辑帮你完成这件事.

下面是一个作用于 Greater 类的类装饰器(@sealed):

1
2
3
4
5
6
7
8
9
10
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}

我们可以通过如下函数声明定义 @sealed 装饰器:

1
2
3
4
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}

@sealed 得以执行, 它会封存构造器及其原型.

下个例子显示怎么覆写构造器.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function classDecorator<T extends {new(...args:any[]):{}}>(constructor:T) {
return class extends constructor {
newProperty = "new property";
hello = "override";
}
}

@classDecorator
class Greeter {
property = "property";
hello: string;
constructor(m: string) {
this.hello = m;
}
}

console.log(new Greeter("world"));

方法装饰器

方法装饰器附加在一个方法声明之前.
方法装饰器作用于方法的属性描述符, 可以监视, 修改, 或替换方法定义.
方法装饰器不能出现在声明文件, 或任何其他外部上下文环境中(比如以 declare 声明的类).

方法装饰器的表达式作为函数在运行期被调用, 给定如下三个参数:

  1. 对静态成员, 类的构造器函数; 对实例成员, 类的原型.
  2. 成员名.
  3. 成员的属性描述符.

注  在低于 ES5 的运行环境中, 属性描述符undefined.

如果方法装饰器返回一个值, 此值成为方法的属性描述符.

注  在低于 ES5 的运行环境中, 返回值将被忽略.

下面是一个作用于 Greater 类一个方法的方法装饰器(@enumerable):

1
2
3
4
5
6
7
8
9
10
11
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}

@enumerable(false)
greet() {
return "Hello, " + this.greeting;
}
}

我们可以通过如下函数声明定义 @enumerable 装饰器:

1
2
3
4
5
function enumerable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.enumerable = value;
};
}

这里的 @enumerable(false) 装饰器是一个装饰器工厂.
@enumerable(false) 被调用时, 它修改属性描述符的 enumerable 属性.

存取方法装饰器

存取方法装饰器附加在一个存取方法声明之前.
存取方法装饰器作用于存取方法的属性描述符, 可以监视, 修改, 或替换存取方法定义.
存取方法装饰器不能出现在声明文件, 或任何其他外部上下文环境中(比如以 declare 声明的类).

注  TypeScript 不允许同时装饰单个成员的 getset 方法.
该成员所有装饰器必须附加在取决于文档顺序的第一个存取方法上.
原因是装饰器作用于属性描述符, 它联合了 getset 两个方法, 并非单独的每一个.

存取方法装饰器的表达式作为函数在运行期被调用, 给定如下三个参数:

  1. 对静态成员, 类的构造器函数; 对实例成员, 类的原型.
  2. 成员名.
  3. 成员的属性描述符.

注  在低于 ES5 的运行环境中, 属性描述符undefined.

如果存取方法装饰器返回一个值, 此值成为目标成员的属性描述符.

注  在低于 ES5 的运行环境中, 返回值将被忽略.

下面是一个作用于 Point 类一个成员的存取方法装饰器(@configurable):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Point {
private _x: number;
private _y: number;
constructor(x: number, y: number) {
this._x = x;
this._y = y;
}

@configurable(false)
get x() { return this._x; }

@configurable(false)
get y() { return this._y; }
}

我们可以通过如下函数声明定义 @configurable 装饰器:

1
2
3
4
5
function configurable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value;
};
}

属性装饰器

属性装饰器附加在一个属性声明之前.
属性装饰器不能出现在声明文件, 或任何其他外部上下文环境中(比如以 declare 声明的类).

属性装饰器的表达式作为函数在运行期被调用, 给定如下两个参数:

  1. 对静态成员, 类的构造器函数; 对实例成员, 类的原型.
  2. 成员名.

注  受制于 TypeScript 初始化属性装饰器的方式, 属性描述符没作为参数提供给属性装饰器.
这是由于当前没有在定义原型成员时描述实例属性的机制, 也没有监视或修改一个属性初始化器的方法. 它的返回值同样被忽略.
故, 属性装饰器只能用来观测一个类已经声明特定成员名的属性.

我们可以利用此信息记录有关属性的元数据, 如下例所示:

1
2
3
4
5
6
7
8
9
10
11
12
class Greeter {
@format("Hello, %s")
greeting: string;

constructor(message: string) {
this.greeting = message;
}
greet() {
let formatString = getFormat(this, "greeting");
return formatString.replace("%s", this.greeting);
}
}

我们可以通过如下函数声明定义 @format 装饰器和 getFormat 函数:

1
2
3
4
5
6
7
8
9
10
11
import "reflect-metadata";

const formatMetadataKey = Symbol("format");

function format(formatString: string) {
return Reflect.metadata(formatMetadataKey, formatString);
}

function getFormat(target: any, propertyKey: string) {
return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}

这里的 @format("Hello, %s") 装饰器是一个装饰器工厂.
@format("Hello, %s") 被调用时, 它使用来自 reflect-metadata 库的 Reflect.metadata 函数为属性添加一条元数据记录.
getFormat 被调用, 它读取元数据获取格式控制串.

注  上例依赖 reflect-metadata 库.
阅读元数据一节获取更多关于 reflect-metadata 库的信息.

参数装饰器

参数装饰器附加在一个参数声明之前.
参数装饰器作用于类构造器函数或方法声明.
参数装饰器不能出现在声明文件, 重载, 或任何其他外部上下文环境中(比如以 declare 声明的类).

参数装饰器的表达式作为函数在运行期被调用, 给定如下三个参数:

  1. 对静态成员, 类的构造器函数; 对实例成员, 类的原型.
  2. 成员名.
  3. 参数在函数参数列表中的编号.

注  参数装饰器只能用来观测一方法已经声明某参数.

参数装饰器的返回值被忽略.

下面是一个作用于 Greeter 类一个成员的参数的参数装饰器(@required):

1
2
3
4
5
6
7
8
9
10
11
12
class Greeter {
greeting: string;

constructor(message: string) {
this.greeting = message;
}

@validate
greet(@required name: string) {
return "Hello " + name + ", " + this.greeting;
}
}

我们可以通过如下函数声明定义 @required@validate 装饰器:

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
import "reflect-metadata";

const requiredMetadataKey = Symbol("required");

function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
existingRequiredParameters.push(parameterIndex);
Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}

function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) {
let method = descriptor.value;
descriptor.value = function () {
let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
if (requiredParameters) {
for (let parameterIndex of requiredParameters) {
if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined) {
throw new Error("Missing required argument.");
}
}
}

return method.apply(this, arguments);
}
}

@required 装饰器添加一条元数据记录标记一个参数是必要的.
然后 @validate 装饰器把现有 greet 方法包裹在一函数中, 在调用源方法前检验所有参数.

注  上例依赖 reflect-metadata 库.
阅读元数据一节获取更多关于 reflect-metadata 库的信息.

元数据

一些例子运用了 reflect-metadata 库, 它为试验性元数据 API添加了一个 polyfill (译注: https://en.wikipedia.org/wiki/Polyfill_(programming)).
这个库还不是 ECMAScript (JavaScript) 标准的一部分.
不过, 只要装饰器正式被采纳成为 ECMAScript 标准的一部分, 这些扩展就会被提议采纳.

你可以通过 npm 安装该库:

1
npm i reflect-metadata --save

TypeScript 包括为有装饰器的声明输出特定类型元数据的试验性支持.
要启用该试验性支持, 你必须在命令行或 tsconfig.json 文件设置 emitDecoratorMetadata 编译器选项.

命令行

1
tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata

tsconfig.json 文件

1
2
3
4
5
6
7
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}

启用后, 只要 reflect-metadata 库已经导入, 额外编译期类型信息就会在运行期可见.

我们可以在下个例子看到这一点:

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
import "reflect-metadata";

class Point {
x: number;
y: number;
}

class Line {
private _p0: Point;
private _p1: Point;

@validate
set p0(value: Point) { this._p0 = value; }
get p0() { return this._p0; }

@validate
set p1(value: Point) { this._p1 = value; }
get p1() { return this._p1; }
}

function validate<T>(target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<T>) {
let set = descriptor.set;
descriptor.set = function (value: T) {
let type = Reflect.getMetadata("design:type", target, propertyKey);
if (!(value instanceof type)) {
throw new TypeError("Invalid type.");
}
set.call(target, value);
}
}

TypeScript 编译器使用 @Reflect.metadata 装饰器注入编译期类型信息.
你可以认为它等同于如下 TypeScript:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Line {
private _p0: Point;
private _p1: Point;

@validate
@Reflect.metadata("design:type", Point)
set p0(value: Point) { this._p0 = value; }
get p0() { return this._p0; }

@validate
@Reflect.metadata("design:type", Point)
set p1(value: Point) { this._p1 = value; }
get p1() { return this._p1; }
}

注  装饰器元数据属于试验特性, 可能将来版本中发生重大改变.

如果这篇文章对您有用,可以考虑打赏:)