typescript--裝飾器

裝飾器

可以對類、類的屬性(數據屬性、訪問器屬性)和方法、方法的參數進行包裝,擴展或修改原來的功能。

目前作爲一項實驗性的特性,需要在tsconfig.json配置中打開Experimental Options選項
/* Experimental Options */
    "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
    "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */,

裝飾器是一種特殊類型的聲明,該聲明可以被附加在類聲明、方法聲明、訪問器屬性、數據屬性或參數上
聲明形式

使用@expression形式來使用,其中expression求值後必須是函數。expression可以直接是函數,也可以是函數調用(該方式稱爲工廠模式)
這裏以類修飾器爲例,如:
expression爲函數時

// 裝飾器,傳入類的構造函數
function testDecorator(constructor: any) {
  // 下面對類進行修飾
  constructor.prototype.getName = () => {
    console.log('增加了getName方法');
  };
  console.log('decorator');
}
// 對類Test進行裝飾,此時的expression即爲裝飾器函數
@testDecorator 
class Test {}

expression爲函數調用(工廠模式時)

// 通過工廠函數的參數flag可以決定是否調用裝飾器
function testFactoryDecorator(flag) {
	if (flag) {
    return function(constructor: any) {
      constructor.prototype.getValue = () => {
        console.log('getValue');
      };
    };
  } else {
    return function(constructor: any) {};
  }
}
@testFactoryDecorator(true)
class Test {}
修飾方式

裝飾器應用到某個聲明x上,可以採用兩種形式
寫在同一行:@f @g x
寫在不同行:

@f
@g
x

當多個裝飾器如上文的@f, @g同時應用到某個聲明x上時,按複合函數的求值順序執行,即進行如下步驟的操作:

  1. 由上至下依次對裝飾器表達式求值
  2. 求值的結果會被當作函數,由下至上依次調用

這也有點像調用棧一樣,求值的過程就像函數入棧,求值結果的函數從調用棧中依次調用。

類中不同聲明上的裝飾器的應用順序:
  • 按類的成員分,按實例成員 -> 靜態成員 -> 構造函數 -> 類的順序
  • 按裝飾器修飾的類型分,按參數裝飾器 -> 方法、訪問符、屬性

即:

  • 參數裝飾器,其次是方法,訪問符,或屬性裝飾器應用到每個實例成員。
  • 參數裝飾器,其次是方法,訪問符,或屬性裝飾器應用到每個靜態成員。
  • 參數裝飾器應用到構造函數。
  • 類裝飾器應用到類。

類裝飾器

聲明方式:類裝飾器在類聲明之前被聲明(緊靠着類聲明)。
  • 類裝飾器應用於類構造函數,可以用來監視,修改或替換類定義。 類裝飾器不能用在聲明文件中(.d.ts),也不能用在任何外部上下文中(比如declare的類)。
  • 類裝飾器表達式會在運行時當作函數被調用,類的構造函數作爲其唯一的參數
  • 如果類裝飾器返回一個值,它會使用提供的構造函數來替換類的聲明
  • 注意如果在裝飾器裏要返回一個新的構造函數,必須注意處理好原來的原型鏈。 在運行時的裝飾器調用邏輯中不會做這些。
類裝飾器的標準寫法
不同的類的constructor是不同的,所以這裏用泛型來寫類型
new () => {} 這裏表示是構造函數,而T繼承構造函數
// 重載構造函數的裝飾器例子
function testStandardDecorator<T extends new (...args: any[]) => any>(
  constructor: T
) {
  // 通過返回一個繼承自constructor的class來進行對constructor的修改,處理好原型鏈關係
  return class extends constructor {
    // 拓展constructor
    name = 'lee'; // 將name屬性值賦值爲'lee'
    getName() {return this.name} // 增加getName()方法
  };
}

@testStandardDecorator
class TestStandard {
  name: string;
  constructor(name: string) {
    this.name = name;
    console.log(this.name); 
  }
}
const test = new TestStandard('hello'); // 先執行constructor,後執行decorator
console.log(test.name); // 先執行constructor,後執行decorator,所以是hello, lee
test.getName(); // typescript會報錯,拓展了構造函數後,不知道爲什麼實例訪問拓展後的方法會報錯

爲了解決裝飾器添加的方法實例訪問報錯的問題,需要按如下方式改寫:

// 裝飾器用工廠模式改寫
function testStandardDecoratorFactory() {
  return function<T extends new (...args: any[]) => any>(constructor: T) {
    // 通過返回一個繼承自constructor的class來進行對constructor的修改
    return class extends constructor {
      // 拓展constructor
      name = 'lee'; // 將name屬性值賦值爲'lee'
      getName() {
        return this.name;
      } // 將getName()方法寫在裝飾器上
    };
  };
}
// 調用裝飾器工廠函數後返回裝飾器函數,再傳入一個class作爲參數,表明裝飾器函數修飾的是該class,最後返回被裝飾過的class
const teststandard = testStandardDecoratorFactory()(
  class {
    name: string;
    constructor(name: string) {
      this.name = name;
      console.log(this.name); 
    }
  }
);

const test = new teststandard('hello');
console.log(test.getName()); // 此時typescript就不會再報錯了

方法裝飾器:方法裝飾器聲明在一個方法的聲明之前(緊靠着方法聲明)。

  • 方法裝飾器聲明在一個方法的聲明之前(緊靠着方法聲明)。 它會被應用到方法的 屬性描述符上,可以用來監視,修改或者替換方法定義。

  • 方法裝飾器不能用在聲明文件( .d.ts),重載或者任何外部上下文(比如declare的類)中。

方法裝飾器會被傳入3個參數,分別是target,key,descriptor

  • target:對於實例方法,target指向類的prototype,對於靜態方法,target指向類的constructor
  • key:指的是方法名
  • descriptor:指的是方法對應的屬性描述符,即該屬性的描述特性,如enumerable(可枚舉)、configurable(可配置)、writable(可修改屬性值)、value(屬性值)
// 實例方法getName()方法的裝飾器,接收參數爲
// target:對應的是類的prototype;
// key: 對應的是被裝飾的方法名;
// descriptor: 對應的是該方法(屬性)的描述符
function getNameDecorator(
  target: any,
  key: string,
  descriptor: PropertyDescriptor
) {
  // console.log(target, key);
  descriptor.value = function() {
    return 'decorator';
  }; // 對getName()方法進行了變更;
}
// 靜態方法getValue()方法的裝飾器,target對應的是類的構造函數
function getValueDecorator(target: any, key: string) {}
class Test3 {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  // 對類中的方法進行裝飾,在類定義好後立即裝飾該方法
  @getNameDecorator
  getName() {
    return this.name;
  }
  // 如果是靜態方法
  @getValueDecorator
  static getValue() {
    return '123';
  }
}

訪問器屬性裝飾器:訪問器裝飾器聲明在一個訪問器的聲明之前(緊靠着訪問器聲明)

  • 訪問器裝飾器應用於訪問器的 屬性描述符並且可以用來監視,修改或替換一個訪問器的定義。 訪問器裝飾器不能用在聲明文件中(.d.ts),或者任何外部上下文(比如 declare的類)裏。
  • 注意  TypeScript不允許同時裝飾一個成員的get和set訪問器。取而代之的是,一個成員的所有裝飾的必須應用在文檔順序的第一個訪問器上。這是因爲,在裝飾器應用於一個屬性描述符時,它聯合了get和set訪問器,而不是分開聲明的。

訪問器裝飾器傳入的參數與方法裝飾器相同。如果訪問器裝飾器返回一個值,它會被用作方法的屬性描述符。

// 訪問器屬性的裝飾器,跟普通實例方法的參數一致
function visitDecorator(
  target: any,
  key: string,
  descriptor: PropertyDescriptor
) {
  descriptor.writable = false; // 不允許重寫訪問器屬性
}
class Test4 {
  private _name: string;
  constructor(name: string) {
    this._name = name;
  }
  // 給訪問器屬性增加裝飾器,訪問器屬性的getter和setter是一組,只能給其中一個增加同名的裝飾器,對這組訪問器屬性有效
  get name() {
    return this._name;
  } //name的getter
  // 給setter增加裝飾器
  @visitDecorator
  set name(name: string) {
    this._name = name;
  }
}
const test4 = new Test4('dell');
// test4.name = '123';
console.log(test4.name);

屬性裝飾器:屬性裝飾器聲明在一個屬性聲明之前(緊靠着屬性聲明)

  • 屬性裝飾器不能用在聲明文件中(.d.ts),或者任何外部上下文(比如 declare的類)裏。

屬性裝飾器傳入2個參數:

  • target:對於靜態成員來說是類的構造函數,對於實例成員是類的原型對象
  • key:屬性名
// 數據屬性的裝飾器,沒有descriptor參數,對實例的數據屬性沒法在裝飾器中修改值
function nameDecorator(target: any, key: string): any {
  const descriptor: PropertyDescriptor = {
    writable: true,
    value: 'lee'
  };
  return descriptor; // 用return的descriptor替換Test5.name屬性的descriptor
}
class Test5 {
  // 給屬性添加裝飾器,改變屬性的descriptor
  @nameDecorator
  name = 'dell';
}
const test5 = new Test5();
console.log(test5.name); // lee

參數裝飾器:參數裝飾器聲明在一個參數聲明之前(緊靠着參數聲明)

  • 參數裝飾器應用於類構造函數或方法聲明
  • 參數裝飾器不能用在聲明文件(.d.ts),重載或其它外部上下文(比如 declare的類)裏。
  • 參數裝飾器的返回值會被忽略。
  • 注意  參數裝飾器只能用來監視一個方法的參數是否被傳入。

參數裝飾器傳入下列3個參數

  • target:對於靜態成員來說是類的構造函數,對於實例成員是類的原型對象
  • key:方法名
  • index:參數的位置
在這裏插入// 參數裝飾器,其中target爲原型,key爲方法名,paramIndex參數表示參數的位置
function paramsDecorator(target: any, key: string, paramIndex: number): any {
  console.log(paramIndex);
}
class Test6 {
  // 對參數進行修飾
  getInfo(@paramsDecorator name: string, age: number) {
    console.log(name, age);
  }
}
const test6 = new Test6();
test6.getInfo('Dell', 30);

元數據

reflect-metadata庫來支持實驗性的metadata API。TypeScript支持爲帶有裝飾器的聲明生成元數據。 你需要在命令行或 tsconfig.json裏啓用emitDecoratorMetadata編譯器選項。
可以給類、類中的方法等利用裝飾器添加元數據。

導入reflect-metadata庫

安裝reflect-metadata包

npm install reflect-metadata --save

在項目文件中導入

import "reflect-metadata"
使用API

可以給對象上定義元數據

const user = {
	name:'xxx'
}
Reflect.defineMetadata('data', 'test', user); // 在user上定義了元數據,鍵值對爲data: 'test',定義在user上。
console.log(Reflect.getMetadata('data', user)); // 通過Reflect.getMetadata方法來獲取

一般應用在類上,類中方法上

// 利用庫本身提供的Reflect.metadata()裝飾器工廠函數,給類User添加裝飾器,定義元數據
@Reflect.metadata('data', 'test')
class User {
	name = 'xxx';
}
// 給類Teacher的getName()方法添加裝飾器,定義元數據
class Teacher {
	@Reflect.metadata('data', 'test')
	getName() {}
}
console.log(Reflect.getMetadata('data', Teacher.prototype, 'getName')); // 獲取到Teacher的getName()方法上定義的元數據
// 自定義一個裝飾器工廠函數
function setData(data: string, msg: string) {
  return function(target: User, key: string) {
    Reflect.defineMetadata(data, msg, target, key);
  };
}
class Teacher {
// 給getAge()方法添加裝飾器,定義元數據
	@setData('data', 'test')
	getAge() {}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章