1. 程式人生 > >TypeScript躬行記(8)——裝飾器

TypeScript躬行記(8)——裝飾器

  裝飾器(Decorator)可宣告在類及其成員(例如屬性、方法等)之上,為它們提供一種標註,用於分離複雜邏輯或附加額外邏輯,其語法形式為@expression。expression是一個會在執行時被呼叫的函式,它的引數是被裝飾的宣告資訊。假設有一個@sealed裝飾器,那麼可以像下面這樣定義sealed()函式。

function sealed(target) {
  //...
}

  有兩種方式可以開啟裝飾器,第一種是在輸入命令時新增--experimentalDecorators引數,如下所示,其中--target引數不能省略,它的值為“ES5”。

tsc default.ts --target ES5 --experimentalDecorators

  第二種是在tsconfig.json配置檔案中新增experimentalDecorators屬性,如下所示,對應的target屬性也不能省略。

{
  compilerOptions: {
    target: "ES5",
    experimentalDecorators: true
  }
}

一、類裝飾器

  類裝飾器用於監聽、修改或替換類的建構函式,並將其作為類裝飾器唯一可接收的引數。當裝飾器返回undefined時,延用原來的建構函式;而當裝飾器有返回值時,會用它來覆蓋原來的建構函式。下面的示例會通過類裝飾器封閉類的建構函式和原型,其中@sealed宣告在類之前。

@sealed
class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}
function sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

  在經過TypeScript編譯後,將會生成一個__decorated()函式,並應用到Person類上,如下所示。

var Person = /** @class */ (function() {
  function Person(name) {
    this.name = name;
  }
  Person = __decorate([sealed], Person);
  return Person;
})();

  注意,類裝飾器不能出現在.d.ts宣告檔案和外部類之中。

二、方法裝飾器

  方法裝飾器宣告在類的方法之前,作用於方法的屬性描述符,比類裝飾器還多一個過載限制。它能接收三個引數,如下所列:

  (1)對於靜態成員來說是類的建構函式,而對於例項成員則是類的原型物件。

  (2)成員的名字,一個字串或符號。

  (3)成員的屬性描述符,當輸出版本低於ES5時,該值將會是undefined。

  當方法裝飾器返回一個值時,會覆蓋當前方法的屬性描述符。下面是一個簡單的例子,方法裝飾器的第一個引數是Person.prototype,第二個是“cover”,呼叫getName()方法得到的將是“freedom”,而不是原先的“strick”。

class Person {
  @cover
  getName(name) {
    return name;
  }
}
function cover(target: any, key: string, descriptor: PropertyDescriptor) {
  descriptor.value = function() {
    return "freedom";
  };
  return descriptor;
}
let person = new Person();
person.getName("strick");        //"freedom"

三、訪問器裝飾器

  訪問器裝飾器宣告在類的訪問器屬性之前,作用於相應的屬性描述符,其限制與類裝飾器相同,而接收的三個引數與方法裝飾器相同。並且還需要注意一點,TypeScript不允許同時裝飾一個成員的get和set訪問器,只能應用在第一個訪問器上。

  以下面的Person類為例,定義了一個訪問器屬性name,當訪問它時,得到的將是“freedom”,而不是原先的“strick”。

class Person {
  private _name: string;
  @access
  get name() {
    return this._name;
  }
  set name(name) {
    this._name = name;
  }
}
function access(target: any, key: string, descriptor: PropertyDescriptor) {
  descriptor.get = function() {
    return "freedom";
  };
  return descriptor;
}
let person = new Person();
person.name = "strick";
console.log(person.name);        //"freedom"

四、屬性裝飾器

  屬性裝飾器宣告在屬性之前,其限制與訪問器裝飾器相同,但只能接收兩個引數,不存在第三個屬性描述符引數,並且沒有返回值。仍然以下面的Person類為例,定義一個name屬性,並且在@property裝飾器中修改其值。

class Person {
  @property
  name: string;
}
function property(target: any, key: string) {
  Object.defineProperty(target, key, {
    value: "freedom"
  });
}
let person = new Person();
person.name = "strick";
console.log(person.name);        //"freedom"

五、引數裝飾器

  引數裝飾器宣告在引數之前,它沒有返回值,其限制與方法裝飾器相同,並且也能接收三個引數,但第三個引數表示裝飾的引數在函式的引數列表中所處的位置(即索引)。下面用一個例子來演示引數裝飾器的用法,需要與方法裝飾器配合。

let params = [];
class Person {
  @func
  getName(@required name) {
    return name;
  }
}

  在@func中呼叫getName()方法,並向其傳入params陣列中的值,@required用於修改指定位置的引數的值,如下所示。

function func(target: any, key: string, descriptor: PropertyDescriptor) {
  const method = descriptor.value;
  descriptor.value = function () {
    return method.apply(this, params);
  };
  return descriptor;
}
function required(target: any, key: string, index: number) {
  params[index] = "freedom";
}

  當例項化Person類,呼叫getName()方法,得到的將是“freedom”。

let person = new Person();
person.getName("strick");        //"freedom"

六、裝飾器工廠

  裝飾器工廠是一個能接收任意個引數的函式,用來包裹裝飾器,使其更易使用,它能返回上述任意一種裝飾器函式。接下來改造方法裝飾器一節中的cover()函式,接收一個字串型別的value引數,返回一個方法裝飾器函式,如下所示。

function cover(value: string) {
  return function(target: any, key: string, descriptor: PropertyDescriptor) {
    descriptor.value = function() {
      return value;
    };
    return descriptor;
  };
}

  在將@cover作用於類中的方法時,需要傳入一個字串,如下所示。

class Person {
  @cover("freedom")
  getName(name) {
    return name;
  }
}

七、裝飾器組合

  將多個裝飾器應用到同一個宣告上時,既可以寫成一行,也可以寫成多行,如下所示。

/****** 一行 ******/
@first @second desc
/****** 多行 ******/
@first 
@second
desc

  這些裝飾器的求值方式與複合函式類似,先由上至下依次執行裝飾器,再將求值結果作為函式,由下至上依次呼叫。例如定義兩個裝飾器工廠函式,如下程式碼所示,在函式體和返回的裝飾器中都會列印一個數字。

function first() {
  console.log(1);
  return function(target: any, key: string, descriptor: PropertyDescriptor) {
    console.log(2);
  };
}
function second() {
  console.log(3);
  return function(target: any, key: string, descriptor: PropertyDescriptor) {
    console.log(4);
  };
}

  將它們先後宣告到類中的同一個方法,如下程式碼所示。根據求值順序可知,先打印出1和3,再打印出4和2。

class Person {
  @first()
  @second()
  getName(name) {
    return name;
  }
}

&n