第三篇的文章中,我們實現了簡單的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類裝飾器標記了類是可以注入的。