前言
随着TypeScript和ES6里引入了类,在一些场景下我们需要额外的特性来支持标注或修改类及其成员。 装饰器(Decorators
)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。 Javascript里的装饰器目前处在 建议征集的第二阶段,但在TypeScript里已做为一项实验性特性予以支持。
详细的基础知识可以参考官方文档,本文也做了一部分截取。
本文结合TypeScript 中的 Decorator & 元数据反射:从小白到专家(部分 I)做一些记录。
关于JS的装饰器可参考 ES7装饰器 Decorator
一、基础
装饰器是一种特殊类型的声明,它能够被附加到 类声明,方法, 访问符,属性 或 参数 上。 装饰器使用@expression
这种形式,expression
求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。
1.1 启用
tsconfig.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
1.2 装饰器工厂
如果我们要定制一个修饰器如何应用到一个声明上,我们得写一个装饰器工厂函数。 装饰器工厂 就是一个简单的函数,它返回一个表达式,以供装饰器在运行时调用。
我们可以通过下面的方式来写一个装饰器工厂函数:
function color(value: string) { // 这是一个装饰器工厂
return function (target) { // 这是装饰器
// do something with "target" and "value"...
}
}
当多个装饰器应用于一个声明上,它们求值方式与复合函数相似。在这个模型下,当复合f和g时,复合的结果(f ∘ g)(x)等同于f(g(x))。
同样的,在TypeScript里,当多个装饰器应用在一个声明上时会进行如下步骤的操作:
- 由上至下依次对装饰器表达式求值。
- 求值的结果会被当作函数,由下至上依次调用。
简单的例子:
function f() {
console.log("f(): evaluated");
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("f(): called");
}
}
function g() {
console.log("g(): evaluated");
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("g(): called");
}
}
class D {
@f()
@g()
method() { }
}
let d = new D();
d.method();
// 输出
// f(): evaluated
// g(): evaluated
// g(): called
// f(): called
二、 几种修饰类型
这些装饰器都
不能用在声明文件中(.d.ts
),或者任何外部上下文(比如 declare
的类)里。
2.1 类装饰器
(和JS类装饰器没有什么区别)
类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。
类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。
如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。
注意 如果你要返回一个新的构造函数,你必须注意处理好原来的原型链。 在运行时的装饰器调用逻辑中 不会为你做这些。
下面是使用类装饰器(@sealed)的例子,应用在Greeter类:
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
下面是一个重载构造函数的例子。
function classDecorator(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"));
// 输出
//class_1 {
// property: 'property',
// hello: 'override',
// newProperty: 'new property' }
可见new时指定的值 world
被覆盖为 override
,并添加了新属性 newProperty
。
2.2 方法装饰器
方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。 它会被应用到方法的 属性描述符上,可以用来监视,修改或者替换方法定义。
方法装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
- 成员的名字。
- 成员的属性描述符。
如果代码输出目标版本小于ES5,属性描述符将会是undefined。
如果方法装饰器返回一个值,它会被用作方法的属性描述符。
如果代码输出目标版本小于ES5返回值会被忽略。
下面是一个方法装饰器(@enumerable)的例子,应用于Greeter类的方法上:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@enumerable(false)
greet() {
return "Hello, " + this.greeting;
}
}
function enumerable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.enumerable = value;
};
}
2.3 访问器get
装饰器
TypeScript不允许同时装饰一个成员的get和set访问器。取而代之的是,一个成员的所有装饰的必须应用在文档顺序的第一个访问器上。这是因为,在装饰器应用于一个属性描述符时,它联合了get和set访问器,而不是分开声明的。
其余特性和 2.2 方法装饰器 一样。
下面是使用了访问器装饰器(@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(true)
get y() { return this._y; }
}
function configurable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value;
};
}
2.4 属性装饰器
属性装饰器表达式会在运行时当作函数被调用,传入下列2个参数:
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
- 成员的名字。
属性描述符不会做为参数传入属性装饰器,这与TypeScript是如何初始化属性装饰器的有关。 因为目前没有办法在定义一个原型对象的成员时描述一个实例属性,并且没办法监视或修改一个属性的初始化方法。返回值也会被忽略。因此,属性描述符只能用来监视类中是否声明了某个名字的属性。
import "reflect-metadata";
const formatMetadataKey = Symbol("format");
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);
}
}
function format(formatString: string) {
return Reflect.metadata(formatMetadataKey, formatString);
}
function getFormat(target: any, propertyKey: string) {
return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}
let geeter = new Greeter('NowhereToRun');
console.log(geeter.greet()); // Hello, NowhereToRun
这个@format("Hello, %s")
装饰器是个 装饰器工厂。当 @format("Hello, %s")
被调用时,它添加一条这个属性的元数据,通过reflect-metadata
库(这个我们后面再说)里的Reflect.metadata
函数。 当 getFormat
被调用时,它读取格式的元数据。
注意 这个例子需要使用
reflect-metadata
库。
2.5 参数装饰器
参数装饰器声明在一个参数声明之前(紧靠着参数声明)。 参数装饰器应用于类构造函数或方法声明。
参数装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
- 成员的名字。
- 参数在函数参数列表中的索引。
注意参数装饰器只能用来监视一个方法的参数是否被传入。
参数装饰器的返回值会被忽略。
import "reflect-metadata";
const requiredMetadataKey = Symbol("required");
function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
console.log(target, propertyKey, parameterIndex);
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) {
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);
}
}
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@validate
greet(@required name: string) {
return "Hello " + name + ", " + this.greeting;
}
}
let greet = new Greeter('u r number1');
console.log(greet.greet('NowhereToRun'));
// Greeter {} 'greet' 0
// Hello NowhereToRun, u r number1
这个例子就是靠@required 和 @validate共同作用,检测参数必须存在,其实在TS的调用时,如果不传参已经会报错了,这里只是一个简单的例子。
@required
装饰器添加了元数据实体把参数标记为必需的。 @validate
装饰器把greet方法包裹在一个函数里在调用原先的函数前验证函数参数。