原文地址 www.tslang.cn
介绍
注释:元编程是指对编程语言的一些特性进行编程。
随着 TypeScript 和 ES6 里引入了类,在一些场景下我们需要额外的特性来支持标注或修改类及其成员。 装饰器(Decorators)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。 Javascript 里的装饰器目前处在 建议征集的第二阶段,但在 TypeScript 里已做为一项实验性特性予以支持。
注意 装饰器是一项实验性特性,在未来的版本中可能会发生改变。
若要启用实验性的装饰器特性,你必须在命令行或tsconfig.json
里启用experimentalDecorators
编译器选项:
命令行:
tsc --target ES5 --experimentalDecorators
tsconfig.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
装饰器工厂
注释:expression 可以是一个返回函数的函数,例如:
@expressionFun(params)
注释:声明是指实现,即定义。
_装饰器_是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。 装饰器使用 @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)
下面是一个重载构造函数的例子。
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"));
方法装饰器
注释:属性描述符:在 JavaScript 中,对象的属性也可以用一些关键字来修饰,用以表示当前属性是否可写、是否有默认值、是否可枚举等,这些关键字就是属性描述符。属性描述符是 ECMAScript 5 新增的语法,它其实就是一个内部对象,用来描述对象的属性的特性。
_方法装饰器_声明在一个方法的声明之前(紧靠着方法声明)。 它会被应用到方法的 _属性描述符_上,可以用来监视,修改或者替换方法定义。 方法装饰器不能用在声明文件 ( .d.ts
),重载或者任何外部上下文(比如declare
的类)中。
方法装饰器表达式会在运行时当作函数被调用,传入下列 3 个参数:
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
- 成员的名字。
- 成员的_属性描述符_。
注意 如果代码输出目标版本小于
ES5
,_属性描述符_将会是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
属性。
访问器装饰器
注释:访问器是指具有 get 或 set 方法的属性。
_访问器装饰器_声明在一个访问器的声明之前(紧靠着访问器声明)。 访问器装饰器应用于访问器的 _属性描述符_并且可以用来监视,修改或替换一个访问器的定义。 访问器装饰器不能用在声明文件中(.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 是如何初始化属性装饰器的有关。 因为目前没有办法在定义一个原型对象的成员时描述一个实例属性,并且没办法监视或修改一个属性的初始化方法。返回值也会被忽略。因此,属性描述符只能用来监视类中是否声明了某个名字的属性。
我们可以用它来记录这个属性的元数据,如下例所示:
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
库更详细的信息。
注释:Reflect 是 ES6 为了操作对象而提供的新 API
- 将Object对象的一些明显属于语言内部的方法,放到Reflect对象上。例如:
Object.defineProperty
- 让Object操作都变成函数行为。某些Object操作是命令式,比如
delete obj[name]
等价于Reflect.deleteProperty(obj, name)
- 当对象的某些行为被Proxy拦截时,通过Reflect调用未拦截前的方法
注释:Relfect Metadata,简单来说,通过装饰器来给类添加一些自定义的信息。然后通过反射将这些信息提取出来。
- Reflect.metadata 是一个函数,接收2个参数。作为装饰器装饰类时,元信息绑定在类这个对象上。装饰属性、方式时原信息绑定在原型上。
- Metadata Key:元数据的 key
- Metadata Value:元数据的值
- 返回一个函数,这个函数也接收2个参数。
- Target:添加元数据的目标对象
- Property:添加元数据的目标对象的属性名- Reflect.getMetadata(key,target,property) 从目标对象 target 或其原型上的属性 property 上获取名为 key 的元信息。
- Reflect.getOwnMetadata(key,target,property) 从目标对象 target 自身的属性 property 上获取名为 key 的元信息。
- Reflect.defineMetadata(key,value,target,property)在目标对象 target 的属性 property 上绑定名为 key 值为 value 的元信息。(绑定到当前对象上而不是其原型上,即使是方法也是。)
@Reflect.metadata('name', 'A') class A { @Reflect.metadata('hello', 'world') public hello(): string { return 'hello world' } } Reflect.getMetadata('name', A) // 'A' Reflect.getMetadata('hello', new A()) // 'world'
注释:在 TS 中也有反射的概念,是指通过类型找到该类型对应的实现。
参数装饰器
_参数装饰器_声明在一个参数声明之前(紧靠着参数声明)。 参数装饰器应用于类构造函数或方法声明。 参数装饰器不能用在声明文件(.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
TypeScript 支持为带有装饰器的声明生成元数据。 你需要在命令行或 tsconfig.json
里启用emitDecoratorMetadata
编译器选项。
Command Line:
tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata
tsconfig.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
当启用后,只要reflect-metadata
库被引入了,设计阶段添加的类型信息可以在运行时使用。
如下例所示:
注释:从访问符的元信息中获取该访问符参数的数据类型,用来控制访问符传参的类型。
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(value);
}
}
TypeScript 编译器可以通过@Reflect.metadata
装饰器注入设计阶段的类型信息。 你可以认为它相当于下面的 TypeScript:
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; }
}
注意 装饰器元数据是个实验性的特性并且可能在以后的版本中发生破坏性的改变(breaking changes)。