第四篇:TypeScript装饰器
第三篇的文章中,我们实现了简单的IoC容器,代码如下:
1 import 'reflect-metadata'; 2 3 type Tag = string; 4 type Constructor<T = any> = new (...args: any[]) => T; 5 type BindValue = string | Function | Constructor<any>; 6 7 export function Injectable(constructor: Object) { 8 9 } 10 11 export class Container { 12 private bindTags: any = {}; 13 public bind(tag: Tag, value: BindValue) { 14 this.bindTags[tag] = value; 15 } 16 public get<T>(tag: Tag): T { 17 const target = this.getTagValue(tag) as Constructor; 18 const providers: BindValue[] = []; 19 for(let i = 0; i < target.length; i++) { 20 // 获取参数的名称 21 const providerKey = Reflect.getMetadata(`design:paramtypes`, target)[i].name; 22 // 把参数的名称作为Tag去取得对应的类 23 const provider = this.getTagValue(providerKey); 24 providers.push(provider); 25 } 26 return new target(...providers.map(p => new (p as Constructor)())) 27 } 28 private getTagValue(tag: Tag): BindValue { 29 const target = this.bindTags[tag]; 30 if (!target) { 31 throw new Error("Can not find the provider"); 32 } 33 return target; 34 } 35 }
1 import { Injectable } from "./container"; 2 3 @Injectable 4 export class Hand { 5 public hit() { 6 console.log('Cust!'); 7 return 'Cut!'; 8 } 9 } 10 @Injectable 11 export class Mouth { 12 public bite() { 13 console.log('Hit!'); 14 return 'Hit!'; 15 } 16 } 17 @Injectable 18 export class Human { 19 private hand: Hand; 20 private mouth: Mouth; 21 constructor( 22 hand: Hand, 23 mouth: Mouth 24 ) { 25 this.hand = hand; 26 this.mouth = mouth; 27 } 28 public fight() { 29 return this.hand.hit(); 30 } 31 public sneak() { 32 return this.mouth.bite(); 33 } 34 }
下面是怎么使用Ioc Container:
1 import 'reflect-metadata'; 2 3 import { Container } from "./container"; 4 import { Hand, Human, Mouth } from "./test-class"; 5 6 const container = new Container(); 7 container.bind('Hand', Hand); 8 container.bind('Mouth', Mouth); 9 container.bind('Human', Human); 10 11 const human = container.get<Human>('Human'); 12 13 human.fight(); 14 human.sneak();
可以看到,bind方法使用的方式有点奇怪,我们使用装饰器来改造一下。
装饰器是一种特殊类型的声明,它能够被附件到类声明,方法,访问符,属性,或者参数上。
装饰器使用@expression这种形式,其中,expression必须是函数。
比如
function sealed(target) { // do something with "target" ... }
如果想要给expression传递一些我们自定义的参数,则需要使用装饰器工厂函数,其实这个装饰器工厂函数就是返回装饰器函数的函数,比如:
1 function color(value: string) { // 这是一个装饰器工厂 2 return function (target) { // 这是装饰器 3 // do something with "target" and "value"... 4 } 5 }
如果有多个装饰器应用在同一个声明上,比如
1 // 书写在同一行 2 @f @g x 3 // 书写在不同行 4 @f 5 @g 6 x
在Typescript中,当多个装饰器应用在一个声明上时,会进行如下步骤的操作。
- 由上至下依次对装饰器表达式求值。
- 求值的结果会被当作装饰器函数,由下至上依次被调用。
1 function f() { 2 console.log("f(): evaluated"); 3 return function (target, propertyKey: string, descriptor: PropertyDescriptor) { 4 console.log("f(): called"); 5 } 6 } 7 8 function g() { 9 console.log("g(): evaluated"); 10 return function (target, propertyKey: string, descriptor: PropertyDescriptor) { 11 console.log("g(): called"); 12 } 13 } 14 15 class C { 16 @f() 17 @g() 18 method() {} 19 }
执行结果
1 f(): evaluated 2 g(): evaluated 3 g(): called 4 f(): called
即求值由上到下,调用由下到上。
那么,如果一个类中有多个装饰器装饰不同的声明,那么它的调用规则是怎么样子的?
类中不同声明上的装饰器将按以下规定的顺序应用:
- 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个实例成员。
- 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个静态成员。
- 参数装饰器应用到构造函数。
- 类装饰器应用到类。
总的来说,从小到大(参数 ==》 方法 ==》 访问符 ==》 属性 ==》 构造函数 ==》 类),和多个装饰器应用到一个声明上的调用规则是一样的。
一.类装饰器
类装饰器应用在类声明前面,可以用来监控,修改,替换类定义。
类装饰器函数只有一个参数,就是这个类的构造函数。
需要特别这样的是,这里的构造函数不是constructor函数,而且构造整个类的函数,
比如下面这个类
1 class Greeter { 2 property = "property"; 3 hello: string; 4 constructor(m: string) { 5 this.hello = m; 6 } 7 }
传递给装饰器函数的不是
1 constructor(m: string) { 2 this.hello = m; 3 }
而是:
1 function Greeter(m) { 2 this.property = "property"; 3 this.hello = m; 4 }
可以理解为就是把整个类传递给了装饰器函数。
类装饰器可以不返回值,这时对类定义进行修改。
1 function sealed(constructor: Function) { 2 Object.seal(constructor); 3 Object.seal(constructor.prototype); 4 } 5 @sealed 6 class Greeter { 7 greeting: string; 8 constructor(message: string) { 9 this.greeting = message; 10 } 11 greet() { 12 return "Hello, " + this.greeting; 13 } 14 }
如果类装饰器返回一个值,那么这个值将会被当作新的构造函数。
需要关注的是,返回的构造函数需要自己处理原型链。
1 function classDecorator<T extends {new(...args:any[]):{}}>(constructor:T) { 2 return class extends constructor { 3 newProperty = "new property"; 4 hello = "override"; 5 } 6 } 7 8 @classDecorator 9 class Greeter { 10 property = "property"; 11 hello: string; 12 constructor(m: string) { 13 this.hello = m; 14 } 15 } 16 17 console.log(new Greeter("world"));
结果是:
1 class_1 { 2 property: 'property', 3 hello: 'override', 4 newProperty: 'new property' 5 }
可以看到,虽然在装饰器中,返回的构造函数extends了用来的构造函数,但是,hello属性的值还是被覆盖掉了。我们来看一下typescript编译成JavaScript的代码:
1 function classDecorator(constructor) { 2 console.log(constructor); 3 return /** @class */ (function (_super) { 4 __extends(class_1, _super); 5 function class_1() {
// 这里,继承了原来的类 6 var _this = _super !== null && _super.apply(this, arguments) || this;
// 这里,新加了一些属性 7 _this.newProperty = "new property";
// 这里,修改了一些属性 8 _this.hello = "override";
// 因为继承了原来的类,使用类中的property属性还是在的 9 return _this; 10 } 11 return class_1; 12 }(constructor)); 13 } 14 var Greeter = /** @class */ (function () {
// 注意这里,传递给装饰器还是的就是整个类,不是普通意义上的构造函数。 15 function Greeter(m) { 16 this.property = "property"; 17 this.hello = m; 18 } 19 Greeter = __decorate([ 20 classDecorator, 21 __metadata("design:paramtypes", [String]) 22 ], Greeter); 23 return Greeter; 24 }());
二.方法装饰器
方法装饰器声明在一个方法的声明之前,它会被应用到方法的属性描述符上。
这里简单记录一下属性描述符:
- value:设置属性值,默认值为 undefined。
- writable:设置属性值是否可写,默认值为 true。
- enumerable:设置属性是否可枚举,即是否允许使用 for/in 语句或 Object.keys() 函数遍历访问,默认为 true。
- configurable:设置是否可设置属性特性,默认为 true。如果为 false,将无法删除该属性,不能够修改属性值,也不能修改属性的属性描述符。
- get:取值函数,默认为 undefined。
- set:存值函数,默认为 undefined。
方法装饰器函数有3个参数:
- 对于静态成员来说是类的构造函数,对于实例成员来说是类的原型对象。
- 成员的名字
- 成员的属性描述符
如果方法装饰器返回一个值,那么这个值会被当作成员的属性描述符。这给成员的定义提供了极大的灵活性。
下面是替换属性描述符的例子:
1 class Greeter { 2 greeting: string; 3 constructor(message: string) { 4 this.greeting = message; 5 } 6 7 @OtherGreet(false) 8 greet() { 9 return "Hello, " + this.greeting; 10 } 11 } 12 function OtherGreet(value: boolean) { 13 return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { 14 console.log(target); 15 return { 16 ...descriptor, 17 value: () => { 18 return 'No Hello,' + target.greeting; 19 } 20 } 21 }; 22 } 23 const g = new Greeter('wt'); 24 console.log(g.greet())
返回的值:
Greeter { greet: [Function] }
No Hello,undefined
这个例子有需要可以思考的地方,第一点,为什么target是Greeter { greet: [Function] },而没有包含greeting. 这是因为,当方法构造器作用在实例成员上时,target是原型对象,而属性是直接作用在对象上的,只有方法是在原型对象上。
我们可以看一下TypeScript的class编译成js的结果:
var Greeter = /** @class */ (function () { function Greeter(message) { this.greeting = message; } // @OtherGreet(false) Greeter.prototype.greet = function () { return "Hello, " + this.greeting; }; return Greeter; }());
可以看到,只有greet方法在原型上。那么,我们怎么才能在方法构造器中访问到类中的其他属性成员呢? 我也不知道。。。。
这也是为什么出现undefined的原因。
三.访问器装饰器
访问器装饰器作用在一个访问器的声明之前,应用于访问器的属性描述符上
访问器装饰器有3个参数:
- 对于静态成员来说是类的构造函数,对于实例成员来说是类的原型对象
- 成员的名字
- 成员的属性描述符
如果访问器装饰器返回一个值,它会被当作新的属性描述符来覆盖原来的属性描述符。
1 class Point { 2 private _x: number; 3 private _y: number; 4 constructor(x: number, y: number) { 5 this._x = x; 6 this._y = y; 7 } 8 9 @configurable(false) 10 get x() { return this._x; } 11 12 @configurable(false) 13 get y() { return this._y; } 14 } 15 function configurable(value: boolean) { 16 return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { 17 descriptor.configurable = value; 18 }; 19 }
四.属性装饰器
属性装饰器声明在一个属性声明之前。
属性装饰器有2个参数:
- 对于静态成员来说是类的构造函数,对于实例成员来说是类的原型对象。
- 成员的名称
注意:属性描述符不会做为参数传入属性装饰器,这与TypeScript是如何初始化属性装饰器的有关。
因为目前没有办法在定义一个原型对象的成员时描述一个实例属性,并且没办法监视或修改一个属性的初始化方法。返回值也会被忽略。
因此,属性描述符只能用来监视类中是否声明了某个名字的属性。
所以,使用属性装饰器时,一般配合元数据一起使用。
1 class Greeter { 2 @format("Hello, %s") 3 greeting: string; 4 5 constructor(message: string) { 6 this.greeting = message; 7 } 8 greet() { 9 let formatString = getFormat(this, "greeting"); 10 return formatString.replace("%s", this.greeting); 11 } 12 } 13 14 15 function format(formatString: string) { 16 return (target: any, propertyName: string) => { 17 Reflect.defineMetadata('format', formatString, target, propertyName ); 18 } 19 } 20 21 function getFormat(target: any, propertyKey: string) { 22 return Reflect.getMetadata('format', target, propertyKey); 23 } 24 const g = new Greeter('wt'); 25 console.log(g.greet());
属性装饰器可以被用来附加一下信息(方法是使用Reflect API),之后再使用API取得设置的信息。
五.参数装饰器
参数装饰器声明在一个参数声明之前,它可以应用于类的构造函数的参数列表或者方法的参数列表中。
参数装饰器有3个参数:
- 对于静态成员来说,是类的构造函数,对于实例成员来说,是类的原型对象。
- 成员的名字(不是参数名称,是函数名称),当是构造函数中的参数时,为undefined,这是因为class被解析成匿名函数了。
- 参数在函数参数列表中的索引(通过索引可以取得是函数中的哪个参数)
1 class Greeter { 2 greeting: string; 3 4 constructor(message: string) { 5 this.greeting = message; 6 } 7 8 @validate 9 greet(@required name: string) { 10 return "Hello " + name + ", " + this.greeting; 11 } 12 } 13 const requiredMetadataKey = Symbol("required"); 14 15 function required(target: Object, propertyKey: string | symbol, parameterIndex: number) { 16 let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || []; 17 existingRequiredParameters.push(parameterIndex); 18 Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey); 19 } 20 21 function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<any>) { 22 let method = descriptor.value; 23 descriptor.value = function () { 24 let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName); 25 if (requiredParameters) { 26 for (let parameterIndex of requiredParameters) { 27 if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined) { 28 throw new Error("Missing required argument."); 29 } 30 } 31 } 32 33 return method.apply(this, arguments); 34 } 35 }
@required
装饰器添加了元数据实体把参数标记为必需的。 @validate
装饰器把greet
方法包裹在一个函数里在调用原先的函数前验证函数参数。
使用
使用装饰器来bind容器
1 import 'reflect-metadata'; 2 3 type Tag = string; 4 type Constructor<T = any> = new (...args: any[]) => T; 5 type BindValue = string | Function | Constructor<any>; 6 7 export class Container { 8 public bindTags: any = {}; 9 public bind(tag: Tag, value: BindValue) { 10 this.bindTags[tag] = value; 11 } 12 public get<T>(tag: Tag): T { 13 const target = this.getTagValue(tag) as Constructor; 14 const providers: BindValue[] = []; 15 for(let i = 0; i < target.length; i++) { 16 // 获取参数的名称 17 const providerKey = Reflect.getMetadata(`design:paramtypes`, target)[i].name; 18 // 把参数的名称作为Tag去取得对应的类 19 const provider = this.getTagValue(providerKey); 20 providers.push(provider); 21 } 22 return new target(...providers.map(p => new (p as Constructor)())) 23 } 24 private getTagValue(tag: Tag): BindValue { 25 const target = this.bindTags[tag]; 26 if (!target) { 27 throw new Error("Can not find the provider"); 28 } 29 return target; 30 } 31 } 32 export const Injectable = (constructor: Constructor) => { 33 container.bind(constructor.name, constructor); 34 } 35 export const container = new Container();
1 import { Injectable } from "./container"; 2 3 @Injectable 4 export class Hand { 5 public hit() { 6 console.log('Cust!'); 7 return 'Cut!'; 8 } 9 } 10 @Injectable 11 export class Mouth { 12 public bite() { 13 console.log('Hit!'); 14 return 'Hit!'; 15 } 16 } 17 @Injectable 18 export class Human { 19 private hand: Hand; 20 private mouth: Mouth; 21 constructor( 22 hand: Hand, 23 mouth: Mouth 24 ) { 25 this.hand = hand; 26 this.mouth = mouth; 27 } 28 public fight() { 29 return this.hand.hit(); 30 } 31 public sneak() { 32 return this.mouth.bite(); 33 } 34 }
1 import 'reflect-metadata'; 2 3 import { container } from "./container"; 4 import { Human } from "./test-class"; 5 const human = container.get<Human>(Human.name); 6 7 human.fight(); 8 human.sneak();
可以看到,这里使用了Injectable类装饰器标记了类是可以注入的。