裝飾器
可以對類、類的屬性(數據屬性、訪問器屬性)和方法、方法的參數進行包裝,擴展或修改原來的功能。
目前作爲一項實驗性的特性,需要在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上時,按複合函數的求值順序執行,即進行如下步驟的操作:
- 由上至下依次對裝飾器表達式求值
- 求值的結果會被當作函數,由下至上依次調用
這也有點像調用棧一樣,求值的過程就像函數入棧,求值結果的函數從調用棧中依次調用。
類中不同聲明上的裝飾器的應用順序:
- 按類的成員分,按實例成員 -> 靜態成員 -> 構造函數 -> 類的順序
- 按裝飾器修飾的類型分,按參數裝飾器 -> 方法、訪問符、屬性
即:
- 參數裝飾器,其次是方法,訪問符,或屬性裝飾器應用到每個實例成員。
- 參數裝飾器,其次是方法,訪問符,或屬性裝飾器應用到每個靜態成員。
- 參數裝飾器應用到構造函數。
- 類裝飾器應用到類。
類裝飾器
聲明方式:類裝飾器在類聲明之前被聲明(緊靠着類聲明)。
- 類裝飾器應用於類構造函數,可以用來監視,修改或替換類定義。 類裝飾器不能用在聲明文件中(.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() {}
}