注意:本文件指的是实验阶段 2 装饰器实现。从 Typescript 5.0 开始,支持阶段 3 装饰器。参见:Typescript 5.0 中的装饰器
介绍
随着 TypeScript 和 ES6 中引入类,现在存在某些场景需要额外的功能来支持注释或修改类和类成员。装饰器提供了一种方法,可以为类声明和成员添加注释和元编程语法。
进一步阅读(第 2 阶段):TypeScript 装饰器完整指南
要启用对装饰器的实验性支持,您必须在命令行或 tsconfig.json
中启用 experimentalDecorators
编译器选项。
命令行:
shell
tsc --target ES5 --experimentalDecorators
tsconfig.json:
{" ": {" ": "ES5"," ": true}}
装饰器
装饰器 是一种特殊的声明,可以附加到 类声明、方法、访问器、属性 或 参数。装饰器使用 @expression
的形式,其中 expression
必须计算为一个函数,该函数将在运行时使用有关装饰声明的信息进行调用。
例如,给定装饰器 @sealed
,我们可以按如下方式编写 sealed
函数
ts
function sealed(target) {// do something with 'target' ...}
装饰器工厂
如果我们想自定义装饰器如何应用于声明,我们可以编写一个装饰器工厂。装饰器工厂 只是一个返回将在运行时由装饰器调用的表达式的函数。
我们可以按以下方式编写装饰器工厂
ts
function color(value: string) {// this is the decorator factory, it sets up// the returned decorator functionreturn function (target) {// this is the decorator// do something with 'target' and 'value'...};}
装饰器组合
多个装饰器可以应用于一个声明,例如在一行上
tsTry
@f @g x
在多行上
tsTry
@f @g x
当多个装饰器应用于单个声明时,它们的评估类似于数学中的函数组合。在这个模型中,当组合函数f和g时,得到的组合(f ∘ g)(x)等效于f(g(x))。
因此,在 TypeScript 中评估单个声明上的多个装饰器时,会执行以下步骤
- 每个装饰器的表达式从上到下进行评估。
- 然后从下到上将结果作为函数调用。
如果我们要使用装饰器工厂,我们可以通过以下示例观察这种评估顺序
tsTry
functionfirst () {console .log ("first(): factory evaluated");return function (target : any,propertyKey : string,descriptor :PropertyDescriptor ) {console .log ("first(): called");};}functionsecond () {console .log ("second(): factory evaluated");return function (target : any,propertyKey : string,descriptor :PropertyDescriptor ) {console .log ("second(): called");};}classExampleClass {@first ()@second ()method () {}}
这将打印以下输出到控制台
shell
first(): factory evaluatedsecond(): factory evaluatedsecond(): calledfirst(): called
装饰器评估
应用于类内部各种声明的装饰器有一个明确的应用顺序
- 对于每个实例成员,应用参数装饰器,然后应用方法、访问器或属性装饰器。
- 对于每个静态成员,应用参数装饰器,然后应用方法、访问器或属性装饰器。
- 对于构造函数,应用参数装饰器。
- 对于类,应用类装饰器。
类装饰器
类装饰器声明在类声明之前。类装饰器应用于类的构造函数,可用于观察、修改或替换类定义。类装饰器不能用于声明文件,也不能用于任何其他环境上下文(例如在 declare
类上)。
类装饰器的表达式将在运行时作为函数调用,其唯一的参数是装饰类的构造函数。
如果类装饰器返回一个值,它将用提供的构造函数替换类声明。
注意:如果您选择返回一个新的构造函数,则必须注意维护原始原型。在运行时应用装饰器的逻辑**不会**为您执行此操作。
以下是一个应用于 BugReport
类的类装饰器(@sealed
)示例
tsTry
@sealed classBugReport {type = "report";title : string;constructor(t : string) {this.title =t ;}}
我们可以使用以下函数声明定义 @sealed
装饰器
ts
function sealed(constructor: Function) {Object.seal(constructor);Object.seal(constructor.prototype);}
当执行 @sealed
时,它将密封构造函数及其原型,因此将阻止在运行时通过访问 BugReport.prototype
或通过在 BugReport
本身(注意 ES2015 类实际上只是基于原型的构造函数的语法糖)上定义属性来添加或删除任何进一步的功能。此装饰器**不会**阻止类对 BugReport
进行子类化。
接下来,我们有一个关于如何覆盖构造函数以设置新默认值的示例。
tsTry
functionreportableClassDecorator <T extends { new (...args : any[]): {} }>(constructor :T ) {return class extendsconstructor {reportingURL = "http://www...";};}@reportableClassDecorator classBugReport {type = "report";title : string;constructor(t : string) {this.title =t ;}}constbug = newBugReport ("Needs dark mode");console .log (bug .title ); // Prints "Needs dark mode"console .log (bug .type ); // Prints "report"// Note that the decorator _does not_ change the TypeScript type// and so the new property `reportingURL` is not known// to the type system:Property 'reportingURL' does not exist on type 'BugReport'.2339Property 'reportingURL' does not exist on type 'BugReport'.bug .; reportingURL
方法装饰器
方法装饰器声明在方法声明之前。装饰器应用于方法的属性描述符,可用于观察、修改或替换方法定义。方法装饰器不能用于声明文件、重载或任何其他环境上下文(例如在 declare
类中)。
方法装饰器的表达式将在运行时作为函数调用,并带有以下三个参数
- 对于静态成员,是类的构造函数,对于实例成员,是类的原型。
- 成员的名称。
- 成员的属性描述符。
注意 如果您的脚本目标低于
ES5
,则属性描述符将为undefined
。
如果方法装饰器返回一个值,它将用作方法的属性描述符。
注意 如果您的脚本目标低于
ES5
,则返回值将被忽略。
以下是一个方法装饰器(@enumerable
)应用于Greeter
类上的方法的示例
tsTry
classGreeter {greeting : string;constructor(message : string) {this.greeting =message ;}@enumerable (false)greet () {return "Hello, " + this.greeting ;}}
我们可以使用以下函数声明来定义@enumerable
装饰器
tsTry
functionenumerable (value : boolean) {return function (target : any,propertyKey : string,descriptor :PropertyDescriptor ) {descriptor .enumerable =value ;};}
这里的@enumerable(false)
装饰器是一个装饰器工厂。当调用@enumerable(false)
装饰器时,它会修改属性描述符的enumerable
属性。
访问器装饰器
访问器装饰器声明在访问器声明之前。访问器装饰器应用于访问器的属性描述符,可用于观察、修改或替换访问器的定义。访问器装饰器不能在声明文件中或任何其他环境上下文中(例如在declare
类中)使用。
注意 TypeScript 不允许为单个成员同时装饰
get
和set
访问器。相反,成员的所有装饰器必须应用于文档顺序中指定的第一个访问器。这是因为装饰器应用于属性描述符,它将get
和set
访问器组合在一起,而不是分别应用于每个声明。
访问器装饰器的表达式将在运行时作为函数调用,并带有以下三个参数
- 对于静态成员,是类的构造函数,对于实例成员,是类的原型。
- 成员的名称。
- 成员的属性描述符。
注意 如果您的脚本目标低于
ES5
,则属性描述符将为undefined
。
如果访问器装饰器返回一个值,它将用作成员的属性描述符。
注意 如果您的脚本目标低于
ES5
,则返回值将被忽略。
以下是一个访问器装饰器(@configurable
)应用于Point
类成员的示例
tsTry
classPoint {private_x : number;private_y : number;constructor(x : number,y : number) {this._x =x ;this._y =y ;}@configurable (false)getx () {return this._x ;}@configurable (false)gety () {return this._y ;}}
我们可以使用以下函数声明来定义@configurable
装饰器
ts
function configurable(value: boolean) {return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {descriptor.configurable = value;};}
属性装饰器
属性装饰器声明在属性声明之前。属性装饰器不能在声明文件中使用,也不能在任何其他环境上下文中使用(例如在declare
类中)。
属性装饰器的表达式将在运行时作为函数调用,并带有以下两个参数
- 对于静态成员,是类的构造函数,对于实例成员,是类的原型。
- 成员的名称。
注意 由于 TypeScript 中属性装饰器的初始化方式,属性描述符不会作为参数提供给属性装饰器。这是因为目前没有机制可以在定义原型成员时描述实例属性,也没有办法观察或修改属性的初始化程序。返回值也被忽略。因此,属性装饰器只能用于观察是否为类声明了特定名称的属性。
我们可以使用此信息来记录有关属性的元数据,如下面的示例所示
ts
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
函数
ts
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
类中)。
参数装饰器的表达式将在运行时作为函数调用,并带有以下三个参数
- 对于静态成员,是类的构造函数,对于实例成员,是类的原型。
- 成员的名称。
- 函数参数列表中参数的序号索引。
注意 参数装饰器只能用于观察方法上是否声明了参数。
参数装饰器的返回值将被忽略。
以下是一个参数装饰器 (@required
) 应用于 BugReport
类成员参数的示例
tsTry
classBugReport {type = "report";title : string;constructor(t : string) {this.title =t ;}@validate required verbose : boolean) {if (verbose ) {return `type: ${this.type }\ntitle: ${this.title }`;} else {return this.title ;}}}
然后我们可以使用以下函数声明定义 @required
和 @validate
装饰器
tsTry
import "reflect-metadata";constrequiredMetadataKey =Symbol ("required");functionrequired (target :Object ,propertyKey : string | symbol,parameterIndex : number) {letexistingRequiredParameters : number[] =Reflect .getOwnMetadata (requiredMetadataKey ,target ,propertyKey ) || [];existingRequiredParameters .push (parameterIndex );Reflect .defineMetadata (requiredMetadataKey ,existingRequiredParameters ,target ,propertyKey );}functionvalidate (target : any,propertyName : string,descriptor :TypedPropertyDescriptor <Function >) {letmethod =descriptor .value !;descriptor .value = function () {letrequiredParameters : number[] =Reflect .getOwnMetadata (requiredMetadataKey ,target ,propertyName );if (requiredParameters ) {for (letparameterIndex ofrequiredParameters ) {if (parameterIndex >=arguments .length ||arguments [parameterIndex ] ===undefined ) {throw newError ("Missing required argument.");}}}returnmethod .apply (this,arguments );};}
@required
装饰器添加一个元数据条目,将参数标记为必需。@validate
装饰器然后将现有的 print
方法包装在一个函数中,该函数在调用原始方法之前验证参数。
注意 此示例需要
reflect-metadata
库。有关reflect-metadata
库的更多信息,请参阅元数据。
元数据
一些示例使用 reflect-metadata
库,该库为 实验性元数据 API 添加了 polyfill。该库尚未成为 ECMAScript (JavaScript) 标准的一部分。但是,一旦装饰器正式被采用为 ECMAScript 标准的一部分,这些扩展将被提议用于采用。
您可以通过 npm 安装此库
shell
npm i reflect-metadata --save
TypeScript 包括对为具有装饰器的声明发出某些类型元数据的实验性支持。要启用此实验性支持,您必须在命令行或 tsconfig.json
中设置 emitDecoratorMetadata
编译器选项
命令行:
shell
tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata
tsconfig.json:
{" ": {" ": "ES5"," ": true," ": true}}
启用后,只要 reflect-metadata
库已导入,额外的设计时类型信息将在运行时公开。
我们可以在以下示例中看到这一点
tsTry
import "reflect-metadata";classPoint {constructor(publicx : number, publicy : number) {}}classLine {private_start :Point ;private_end :Point ;@validate setstart (value :Point ) {this._start =value ;}getstart () {return this._start ;}@validate setend (value :Point ) {this._end =value ;}getend () {return this._end ;}}functionvalidate <T >(target : any,propertyKey : string,descriptor :TypedPropertyDescriptor <T >) {letset =descriptor .set !;descriptor .set = function (value :T ) {lettype =Reflect .getMetadata ("design:type",target ,propertyKey );if (!(value instanceoftype )) {throw newTypeError (`Invalid type, got ${typeofvalue } not ${type .name }.`);}set .call (this,value );};}constline = newLine ()line .start = newPoint (0, 0)// @ts-ignore// line.end = {}// Fails at runtime with:// > Invalid type, got object not Point
TypeScript 编译器将使用 @Reflect.metadata
装饰器注入设计时类型信息。您可以将其视为以下 TypeScript 的等效项
ts
class Line {private _start: Point;private _end: Point;@validate@Reflect.metadata("design:type", Point)set start(value: Point) {this._start = value;}get start() {return this._start;}@validate@Reflect.metadata("design:type", Point)set end(value: Point) {this._end = value;}get end() {return this._end;}}
注意 装饰器元数据是一个实验性功能,可能会在将来的版本中引入重大更改。