装饰器
状态: 初稿
介绍
随着 TypeScript 和 ES6 对类的引入, 现在开始出现一些要求能注解或修改类和类成员的额外特性的场景.
装饰器提供一种途径, 为类声明及其成员添加注解和元编程语法.
装饰器是一个 JavaScript 第二阶段提议, 以及 TypeScript 试验特性.
注 装饰器作为试验特性, 可能在将来版本中发生改变.
要启用对装饰器的试验性支持, 你必须在命令行或 tsconfig.json
文件启用 experimentalDecorators
编译器选项.
命令行:
1 | tsc --target ES5 --experimentalDecorators |
tsconfig.json 文件
1 | { |
装饰器
装饰器是可以附加在类声明, 方法, 存取方法, 属性, 或参数上的一类特殊声明.
装饰器满足 @expression
形式, 其中, expression
必须当成函数求值, 在运行期, 求出的函数被调用, 其参数是关于被装饰声明的信息.
例如, 给定装饰器 @sealed
, 我们可能会编写如下 sealed
函数:
1 | function sealed(target) { |
注 你可以在后文类装饰器一节看到装饰器更详细的实例.
装饰器工厂
如果我们想自定义一个装饰器如何附加到一声明上, 可以编写装饰器工厂.
装饰器工厂只是一个函数, 它返回一个在运行期由装饰器调用的表达式.
我们可以参考以下样式编写装饰器工厂:
1 | function color(value: string) { // this is the decorator factory |
注 你可以在后文方法装饰器一节看到装饰器工厂更详细的实例.
装饰器复合
多个装饰器可一并附加到同一个声明上, 如下例所示:
- 置于单行:
1 | x |
- 置于多行:
1 |
|
当多个装饰器附加于单一声明, 它们的求值过程十分类似数学中的复合函数. 在这个例子中, 我们复合函数 f 和 g, 结果 (f ∘ g)(x) 等同于 f(g(x)).
由此, TypeScript 执行以下步骤求值附加在单一声明的多个装饰器:
- 从上往下求出每个装饰器的表达式.
- 从下往上调用求出的函数.
如果我们会用装饰器工厂, 可以通过下例观察求值顺序:
1 | function f() { |
它会在终端打印下列输出:
1 | f(): evaluated |
对装饰器求值
附加到类中各种声明上的装饰器的应用顺序已经预先定义:
- 附加在每个实例成员上的参数装饰器, 然后是方法, 存取方法, 或属性装饰器.
- 附加在每个静态成员上的参数装饰器, 然后是方法, 存取方法, 或属性装饰器.
- 附加在构造器上的参数装饰器.
- 附加在类上的类装饰器.
类装饰器
类装饰器附加在一个类声明之前.
类装饰器作用于类的构造器, 可以监视, 修改, 或替换类定义.
类装饰器不能出现在声明文件, 或任何其他外部上下文环境中(比如以 declare
声明的类).
类装饰器的表达式作为函数在运行期被调用, 目标类的构造器是它唯一的参数.
如果类装饰器返回一个值, 它会用给定构造器函数替换类声明.
注 如果你选择返回新构造器函数, 你必须维护好类的旧原型.
运行期应用装饰器的逻辑不帮你完成这件事.
下面是一个作用于 Greater
类的类装饰器(@sealed
):
1 |
|
我们可以通过如下函数声明定义 @sealed
装饰器:
1 | function sealed(constructor: Function) { |
当 @sealed
得以执行, 它会封存构造器及其原型.
下个例子显示怎么覆写构造器.
1 | function classDecorator<T extends {new(...args:any[]):{}}>(constructor:T) { |
方法装饰器
方法装饰器附加在一个方法声明之前.
方法装饰器作用于方法的属性描述符, 可以监视, 修改, 或替换方法定义.
方法装饰器不能出现在声明文件, 或任何其他外部上下文环境中(比如以 declare
声明的类).
方法装饰器的表达式作为函数在运行期被调用, 给定如下三个参数:
- 对静态成员, 类的构造器函数; 对实例成员, 类的原型.
- 成员名.
- 成员的属性描述符.
注 在低于 ES5 的运行环境中, 属性描述符是
undefined
.
如果方法装饰器返回一个值, 此值成为方法的属性描述符.
注 在低于 ES5 的运行环境中, 返回值将被忽略.
下面是一个作用于 Greater
类一个方法的方法装饰器(@enumerable
):
1 | class Greeter { |
我们可以通过如下函数声明定义 @enumerable
装饰器:
1 | function enumerable(value: boolean) { |
这里的 @enumerable(false)
装饰器是一个装饰器工厂.@enumerable(false)
被调用时, 它修改属性描述符的 enumerable
属性.
存取方法装饰器
存取方法装饰器附加在一个存取方法声明之前.
存取方法装饰器作用于存取方法的属性描述符, 可以监视, 修改, 或替换存取方法定义.
存取方法装饰器不能出现在声明文件, 或任何其他外部上下文环境中(比如以 declare
声明的类).
注 TypeScript 不允许同时装饰单个成员的
get
和set
方法.
该成员所有装饰器必须附加在取决于文档顺序的第一个存取方法上.
原因是装饰器作用于属性描述符, 它联合了get
和set
两个方法, 并非单独的每一个.
存取方法装饰器的表达式作为函数在运行期被调用, 给定如下三个参数:
- 对静态成员, 类的构造器函数; 对实例成员, 类的原型.
- 成员名.
- 成员的属性描述符.
注 在低于 ES5 的运行环境中, 属性描述符是
undefined
.
如果存取方法装饰器返回一个值, 此值成为目标成员的属性描述符.
注 在低于 ES5 的运行环境中, 返回值将被忽略.
下面是一个作用于 Point
类一个成员的存取方法装饰器(@configurable
):
1 | class Point { |
我们可以通过如下函数声明定义 @configurable
装饰器:
1 | function configurable(value: boolean) { |
属性装饰器
属性装饰器附加在一个属性声明之前.
属性装饰器不能出现在声明文件, 或任何其他外部上下文环境中(比如以 declare
声明的类).
属性装饰器的表达式作为函数在运行期被调用, 给定如下两个参数:
- 对静态成员, 类的构造器函数; 对实例成员, 类的原型.
- 成员名.
注 受制于 TypeScript 初始化属性装饰器的方式, 属性描述符没作为参数提供给属性装饰器.
这是由于当前没有在定义原型成员时描述实例属性的机制, 也没有监视或修改一个属性初始化器的方法. 它的返回值同样被忽略.
故, 属性装饰器只能用来观测一个类已经声明特定成员名的属性.
我们可以利用此信息记录有关属性的元数据, 如下例所示:
1 | class Greeter { |
我们可以通过如下函数声明定义 @format
装饰器和 getFormat
函数:
1 | import "reflect-metadata"; |
这里的 @format("Hello, %s")
装饰器是一个装饰器工厂.
在 @format("Hello, %s")
被调用时, 它使用来自 reflect-metadata
库的 Reflect.metadata
函数为属性添加一条元数据记录.
当 getFormat
被调用, 它读取元数据获取格式控制串.
注 上例依赖
reflect-metadata
库.
阅读元数据一节获取更多关于reflect-metadata
库的信息.
参数装饰器
参数装饰器附加在一个参数声明之前.
参数装饰器作用于类构造器函数或方法声明.
参数装饰器不能出现在声明文件, 重载, 或任何其他外部上下文环境中(比如以 declare
声明的类).
参数装饰器的表达式作为函数在运行期被调用, 给定如下三个参数:
- 对静态成员, 类的构造器函数; 对实例成员, 类的原型.
- 成员名.
- 参数在函数参数列表中的编号.
注 参数装饰器只能用来观测一方法已经声明某参数.
参数装饰器的返回值被忽略.
下面是一个作用于 Greeter
类一个成员的参数的参数装饰器(@required
):
1 | class Greeter { |
我们可以通过如下函数声明定义 @required
和 @validate
装饰器:
1 | import "reflect-metadata"; |
@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 | { |
启用后, 只要 reflect-metadata
库已经导入, 额外编译期类型信息就会在运行期可见.
我们可以在下个例子看到这一点:
1 | import "reflect-metadata"; |
TypeScript 编译器使用 @Reflect.metadata
装饰器注入编译期类型信息.
你可以认为它等同于如下 TypeScript:
1 | class Line { |
注 装饰器元数据属于试验特性, 可能将来版本中发生重大改变.