裝飾器與元數據反射(3)參數裝飾器

之前已經分別介紹了方法裝飾器屬性裝飾器和類裝飾器,這篇文章我們來繼續關注這些話題:

  • 參數裝飾器
  • 裝飾器工廠

我們將圍繞以下這個例子,來探討這些概念:

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;

如下我們爲類PersonsaySomething方法的參數添加一個參數裝飾器

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!");
  }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章