第四篇: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 }
container.ts
 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 }
test-class.ts

下面是怎么使用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. 由上至下依次對裝飾器表達式求值。
  2. 求值的結果會被當作裝飾器函數,由下至上依次被調用。
 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

 

 即求值由上到下,調用由下到上。

那么,如果一個類中有多個裝飾器裝飾不同的聲明,那么它的調用規則是怎么樣子的?

 

類中不同聲明上的裝飾器將按以下規定的順序應用:

  1. 參數裝飾器,然后依次是方法裝飾器訪問符裝飾器,或屬性裝飾器應用到每個實例成員。
  2. 參數裝飾器,然后依次是方法裝飾器訪問符裝飾器,或屬性裝飾器應用到每個靜態成員。
  3. 參數裝飾器應用到構造函數。
  4. 類裝飾器應用到類。

總的來說,從小到大(參數 ==》 方法 ==》 訪問符 ==》 屬性 ==》 構造函數 ==》 類),和多個裝飾器應用到一個聲明上的調用規則是一樣的。

一.類裝飾器

類裝飾器應用在類聲明前面,可以用來監控,修改,替換類定義。

類裝飾器函數只有一個參數,就是這個類的構造函數

需要特別這樣的是,這里的構造函數不是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. 對於靜態成員來說是類的構造函數,對於實例成員來說是類的原型對象。
  2. 成員的名字
  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. 對於靜態成員來說是類的構造函數,對於實例成員來說是類的原型對象
  2. 成員的名字
  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個參數:

  1. 對於靜態成員來說是類的構造函數,對於實例成員來說是類的原型對象。
  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個參數:

  1. 對於靜態成員來說,是類的構造函數,對於實例成員來說,是類的原型對象。
  2. 成員的名字(不是參數名稱,是函數名稱),當是構造函數中的參數時,為undefined,這是因為class被解析成匿名函數了。
  3. 參數在函數參數列表中的索引(通過索引可以取得是函數中的哪個參數)
 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類裝飾器標記了類是可以注入的。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM