之前已經分別介紹了方法裝飾器、屬性裝飾器和類裝飾器,這篇文章我們來繼續關注這些話題:
- 引數裝飾器
- 裝飾器工廠
我們將圍繞以下這個例子,來探討這些概念:
class Person {
public name: string;
public surname: string;
constructor(name : string, surname : string) {
this.name = name;
this.surname = surname;
}
public saySomething(something : string) : string {
return this.name + " " + this.surname + " says: " + something;
}
}
引數裝飾器
TypeScript對於引數裝飾器的宣告如下
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;
如下我們為類Person
的saySomething
方法的引數新增一個引數裝飾器
public saySomething(@logParameter something : string) : string {
return this.name + " " + this.surname + " says: " + something;
}
最終被編譯為JavaScript的樣子為:
Object.defineProperty(Person.prototype, "saySomething",
__decorate(
[__param(0, logParameter)],
Person.prototype,
"saySomething",
Object.getOwnPropertyDescriptor(Person.prototype, "saySomething")
)
);
return Person;
如果將其和之前的裝飾器比較,是否會發現又使用了Object.defineProperty()
方法,那麼是否意味著saySomething
將被__decorated
函式的返回值替換?
我們發現這裡有個新函式__param
,TypeScript編譯器生成如下:
var __param = this.__param || function (index, decorator) {
// 返回一個裝飾器函式
return function (target, key) {
// 應用裝飾器(忽略返回值)
decorator(target, key, index);
}
};
如上所示,呼叫引數裝飾器,其並沒有返回值,這就意味著,函式__decorate
的呼叫返回並沒有覆蓋方法saySomething
,也很好理解:引數裝飾器要毛返回。
可見引數裝飾器函式需要3個引數:被裝飾類的原型,裝飾引數所屬的方法名,引數的索引。具體的實現如下:
function logParameter(target: any, key : string, index : number) {
var metadataKey = `log_${key}_parameters`;
if (Array.isArray(target[metadataKey])) {
target[metadataKey].push(index);
}
else {
target[metadataKey] = [index];
}
}
其中向類的原型中增加一個新的屬性metadataKey
,該屬性值是一個陣列,包含所裝飾引數的索引,可以把它當作後設資料。
引數裝飾器不應當用來修改構造器、方法或屬性的行為,它只應當用來產生某種後設資料。一旦後設資料被建立,我們便可以用其它的裝飾器去讀取它。
裝飾器工廠
官方TypeScript裝飾器建議定義一個如下的裝飾器工廠:
裝飾器工廠首先是一個函式,它接受任意數量的引數,同時返回如前所述的四種之一特定型別的裝飾器。
雖然已經討論四種裝飾是如何實現及使用的,但還是有一些可以改進的地方,觀察下面的程式碼片段:
@logClass
class Person {
@logProperty
public name: string;
public surname: string;
constructor(name : string, surname : string) {
this.name = name;
this.surname = surname;
}
@logMethod
public saySomething(@logParameter something : string) : string {
return this.name + " " + this.surname + " says: " + something;
}
}
這裡裝飾器的使用是沒問題的,但如果我們可以不關心裝飾器的型別,而在任何地方使用豈不方便,就像下面的樣子:
@log
class Person {
@log
public name: string;
public surname: string;
constructor(name : string, surname : string) {
this.name = name;
this.surname = surname;
}
@log
public saySomething(@log something : string) : string {
return this.name + " " + this.surname + " says: " + something;
}
}
這邊是裝飾器工廠的使用訴求,它可以識別具體情況下該使用哪種型別的裝飾器,幸運的是,我們可以通過傳遞給裝飾器的引數來區分它的型別。
function log(...args : any[]) {
switch(args.length) {
case 1:
return logClass.apply(this, args);
case 2:
return logProperty.apply(this, args);
case 3:
if(typeof args[2] === "number") {
return logParameter.apply(this, args);
}
return logMethod.apply(this, args);
default:
throw new Error("Decorators are not valid here!");
}
}