随着TypeScript和ES6里引入了类,现在在一些场景下我们会需要额外的特性,用来支持标注或修改类及其成员。 Decorators提供了一种在类的声明和成员上使用元编程语法添加标注的方式。 Javascript里的Decorators目前处在建议征集的第一阶段,在TypeScript里做为实验性特性已经提供了支持。
注意 Decorators是实验性的特性,在未来的版本中可能会发生改变。
若要启用实验性的decorator,你必须启用experimentalDecorators
编译器选项,在命令行中或在tsconfig.json
:
命令行:
tsc --target ES5 --experimentalDecorators
tsconfig.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
Decorators (后文译作装饰器)
装饰器是一种特殊类型的声明,它能够被附加到类声明,方法,访问符,属性,或 参数上。
装饰器利用@expression
这种方式,expression
求值后必须为一个函数,它使用被装饰的声明信息在运行时被调用。
例如,有一个@sealed
装饰器,我们会这样定义sealed
函数:
function sealed(target) {
// do something with "target" ...
}
注意 下面类装饰器小节里有一个更加详细的例子。
装饰器工厂
如果我们想自定义装饰器是如何作用于声明的,我们得写一个装饰器工厂函数。 装饰器工厂就是一个简单的函数,它返回一个表达式,以供装饰器在运行时调用。
我们可以通过下面的方式来写一个装饰器工厂
function color(value: string) { // 这是一个装饰器工厂
return function (target) { // 这是装饰器
// do something with "target" and "value"...
}
}
注意 下面方法装饰器小节里有一个更加详细的例子。
装饰器组合
多个装饰器可以同时应用到一个声明上,就像下面的示例:
-
写在同一行上:
@f @g x
-
写在多行上:
@f @g x
当多个装饰器应用于一个声明上,它们求值方式与复合函数相似。在这个模型下,当复合f和g时,复合的结果(f ∘ g)(x)等同于f(g(x))。
同样的,在TypeScript里,当多个装饰器应用在一个声明上时会进行如下步骤的操作:
- 由上至下依次对装饰器表达式求值。
- 求值的结果会被当作函数,由下至上依次调用。
如果我们使用装饰器工厂的话,可以通过下面的例子来观察它们求值的顺序:
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() {}
}
在控制台里会打印出如下结果:
f(): evaluated
g(): evaluated
g(): called
f(): called
装饰器求值
类中不同声明上的装饰器将按以下规定的顺序应用:
- 参数装饰器,其次是方法,访问符,或属性装饰器应用到每个实例成员。
- 参数装饰器,其次是方法,访问符,或属性装饰器应用到每个静态成员。
- 参数装饰器应用到构造函数。
- 类装饰器应用到类。
类装饰器
类装饰器在类声明之前被声明(紧贴着类声明)。
类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。
类装饰器不能用在声明文件中(.d.ts
),也不能用在任何外部上下文中(比如declare
的类)。
类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。
如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。
注意 如果你要返回一个新的构造函数,你必须注意处理好原来的原型链。 在运行时的装饰器调用逻辑中不会为你做这些。
下面是使用类装饰器(@sealed
)的例子,应用到Greeter
类:
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
我们可以这样定义@sealed
装饰器
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
当@sealed
被执行的时候,它将密封此类的构造函数和原型。(注:参见Object.seal)
方法装饰器
方法装饰器声明在一个方法的声明之前(紧贴着方法声明)。
它会被应用到方法的属性描述符上,可以用来监视,修改或者替换方法定义。
方法装饰器不能用在声明文件(.d.ts
),重载或者任何外部上下文(比如declare
的类)中。
方法装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
- 成员的名字。
- 成员的属性描述符。
注意 如果代码输出目标版本小于
ES5
,Property Descriptor将会是undefined
。
如果方法装饰器返回一个值,它会被用作方法的属性描述符。
注意 如果代码输出目标版本小于
ES5
返回值会被忽略。
下面是一个方法装饰器(@enumerable
)的例子,应用于Greeter
类的方法上:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@enumerable(false)
greet() {
return "Hello, " + this.greeting;
}
}
我们可以用下面的函数声明来定义@enumerable
装饰器:
function enumerable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.enumerable = value;
};
}
这里的@enumerable(false)
是一个装饰器工厂。
当装饰器@enumerable(false)
被调用时,它会修改属性描述符的enumerable
属性。
访问符装饰器
访问符装饰器声明在一个访问符的声明之前(紧贴着访问符声明)。
访问符装饰器应用于访问符的属性描述符并且可以用来监视,修改或替换一个访问符的定义。
访问符装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如declare
的类)里。
注意 TypeScript不允许同时装饰一个成员的
get
和set
访问符。相反,所有装饰的成员必须被应用到文档顺序指定的第一个访问符。这是因为,装饰器应用于一个属性描述符,它联合了get
和set
访问符,而不是分开声明的。
访问符装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
- 成员的名字。
- 成员的属性描述符。
注意 如果代码输出目标版本小于
ES5
,Property Descriptor将会是undefined
。
如果访问符装饰器返回一个值,它会被用作方法的属性描述符。
注意 如果代码输出目标版本小于
ES5
返回值会被忽略。
下面是使用了访问符装饰器(@configurable
)的例子,应用于Point
类的成员上:
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
装饰器:
function configurable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value;
};
}
属性装饰器
属性装饰器声明在一个属性声明之前(紧贴着属性声明)。
属性装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如declare
的类)里。
属性装饰器表达式会在运行时当作函数被调用,传入下列2个参数:
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
- 成员的名字。
注意 属性描述符不会做为参数传入属性装饰器,这与TypeScript是如何初始化属性装饰器的有关。 因为目前没有办法在定义一个原型对象的成员时描述一个实例属性,并且没办法监视或修改一个属性的初始化方法。 因此,属性描述符只能用来监视类中是否声明了某个名字的属性。
如果属性装饰器返回一个值,它会被用作方法的属性描述符。
注意 如果代码输出目标版本小于
ES5
,返回值会被忽略。
如果访问符装饰器返回一个值,它会被用作方法的属性描述符。
我们可以用它来记录这个属性的元数据,如下例所示:
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
函数:
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
库更详细的信息。
参数装饰器
参数装饰器声明在一个参数声明之前(紧贴着参数声明)。
参数装饰器应用于类构造函数或方法声明。
参数装饰器不能用在声明文件(.d.ts),重载或其它外部上下文(比如declare
的类)里。
参数装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
- 成员的名字。
- 参数在函数参数列表中的索引。
注意 参数装饰器只能用来监视一个方法的参数是否被传入。
参数装饰器的返回值会被忽略。
下例定义了参数装饰器(@required
)并应用于Greeter
类方法的一个参数:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@validate
greet(@required name: string) {
return "Hello " + name + ", " + this.greeting;
}
}
然后我们使用下面的函数定义 @required
和 @validate
装饰器:
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
库来支持实验性的 metadata API。
这个库还不是ECMAScript (JavaScript)标准的一部分。
然而,当装饰器被ECMAScript官方标准采纳后,这些扩展也将被推荐给ECMAScript以采纳。
你可以通过npm安装这个库:
npm i reflect-metadata --save