裝飾器(Decorators)可用來裝飾類,屬性,及方法,甚至是函數的參數,以改變和控制這些對象的表現,獲得一些功能。
裝飾器以 @expression
形式呈現在被裝飾對象的前面或者上方,其中 expression
為一個函數,根據其所裝飾的對象的不同,得到的入參也不同。
以下兩種風格均是合法的:
@f @g x
@f
@g
x
ES 中裝飾器處於 Stage 2 階段 ,TypeScript 中通過開啟相應編譯開關來使用。
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
一個簡單的示例
一個簡單的示例,展示了 TypeScript 中如何編寫和使用裝飾器。
function log(
_target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function() {
console.log(`method ${propertyKey} called`);
return originalMethod.apply(this, arguments);
};
}
class Test {
@log
static sayHello() {
console.log("hello");
}
}
Test.sayHello();
上面的示例中,創建了名為 log
的方法,它將作為裝飾器作用於類的方法上,在方法被調用時輸出一條日志。作為裝飾器的 log
函數其入參在后面會介紹。
執行結果:
method sayHello called
hello
裝飾器的工廠方法
上面的裝飾器比較呆板,設想我們想將它變得更加靈活和易於復用一些,則可以通過創建一個工廠方法來實現。因為本質上裝飾器就是個普通函數,函數可通過另外的函數來創建和返回,同時裝飾器的使用本質上也是一個函數調用。通過傳遞給工廠方法不同的參數,以獲得不同表現的裝飾器。
function logFactory(prefix: string) {
return function log(
_target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function() {
console.log(`method ${propertyKey} called`);
return originalMethod.apply(this, arguments);
};
};
}
class Test {
@logFactory("[debug]")
static sayHello() {
console.log("hello");
}
@logFactory("[info]")
static sum() {
return 1 + 1;
}
}
Test.sayHello();
Test.sum();
執行結果:
[debug] method sayHello called
hello
[info] method sum called
多個裝飾器
多個裝飾器可同時作用於同一對象,按順序書寫出需要運用的裝飾器即可。其求值(evaluate)和真正被執行(call)的順序是反向的。即,排在前面的先求值,排在最后的先執行。
譬如,
function f() {
console.log("f(): evaluated");
return function(target, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("f(): called");
};
}
function g() {
console.log("g(): evaluated");
return function(target, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("g(): called");
};
}
class C {
@f()
@g()
method() {}
}
求值 的過程就體現在裝飾器可能並不直接是一個可調用的函數,而是一個工廠方法或其他表達式,只有在這個工廠方法或表達式被求值后,才得到真正被調用的裝飾器。
所以在這個示例中,先依次對 f()
g()
求值,再從 g()
開始執行到 f()
。
運行結果:
f(): evaluated
g(): evaluated
g(): called
f(): called
不同類型的裝飾器
類的裝飾器
作用於類(Class)上的裝飾器,用於修改類的一些屬性。如果裝飾器有返回值,該返回值將替換掉該類的聲明而作為新的構造器使用。
裝飾器入參:
- 類的構造器。
示例:
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
@sealed
將類進行密封,將無法再向類添加屬性,同時類上屬性也變成不可配置的(non-configurable)。
另一個示例:
function classDecorator<T extends { new (...args: any[]): {} }>(
constructor: T
) {
return class extends constructor {
newProperty = "new property";
hello = "override";
};
}
@classDecorator
class Greeter {
property = "property";
hello: string;
constructor(m: string) {
this.hello = m;
}
}
console.log(new Greeter("world"));
因為 @classDecorator
中有返回值,這個值將替換本來類的定義,當 new
的時候,使用的是裝飾器中返回的構造器來創建類。
方法的裝飾器
裝飾器作用於類的方法時可用於觀察,修改或替換該方法。如果裝飾器有返回值,將替換掉被作用方法的屬性描述器(roperty Descriptor)。
裝飾器入參依次為:
- 作用於靜態方法時為類的構造器,實例方法時為類的原型(prototype)。
- 被作用的方法的名稱。
- 被作用對象的屬性描述器。
示例:
function enumerable(value: boolean) {
return function(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
descriptor.enumerable = value;
};
}
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@enumerable(false)
greet() {
return "Hello, " + this.greeting;
}
}
上面示例中 @enumerable
改變了被裝飾方法的 enumerable
屬性,控制其是否可枚舉。
類的方法可以是設置器(setter)或獲取器(getter)。當兩者成對出現時,應當只對其中一個運用裝飾器,誰先出現就用在誰身上。因為裝飾器應用時是用在 get
和 set
兩者合並的屬性描述器上的。
class Test {
private _foo = 1;
@logFactory("[info]")
get foo() {
return this._foo;
}
//🚨 Decorators cannot be applied to multiple get/set accessors of the same name.ts(1207)
@logFactory("[info]")
set foo(val: number) {
this._foo = val;
}
}
屬性的裝飾器
作用於類的屬性時,其入參依次為:
- 如果裝飾的是靜態屬性則為類的構造器,實例屬性則為類的原型
- 屬性名
此時並沒有提供第三個入參,即該屬性的屬性描述器。因為定義屬性時,沒有相應機制來描述該屬性,同時屬性初始化時也沒有方式可以對其進行修改或觀察。
如果裝飾器有返回值,將被忽略。
因此,屬性裝飾器僅可用於觀察某個屬性是否被創建。
一個示例:
function logProperty(target: any, key: string) {
// property value
var _val = this[key];
// property getter
var getter = function() {
console.log(</span>Get: ${<span class="pl-smi">key</span>} => ${<span class="pl-smi">_val</span>}<span class="pl-pds">
);
return _val;
};
// property setter
var setter = function(newVal) {
console.log(</span>Set: ${<span class="pl-smi">key</span>} => ${<span class="pl-smi">newVal</span>}<span class="pl-pds">
);
_val = newVal;
};
// Delete property.
if (delete this[key]) {
// Create new property with getter and setter
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
}
class Person {
@logProperty
public name: string;
public surname: string;
constructor(name: string, surname: string) {
this.name = name;
this.surname = surname;
}
}
var p = new Person("remo", "Jansen");
p.name = "Remo";
var n = p.name;
這個示例中,通過將原屬性刪除,創建帶設置器和獲取器的同名屬性,來達到對屬性值變化的監聽。注意此時操作的已經不是最初那個屬性了。
運行結果:
Set: name => remo
Set: name => Remo
Get: name => Remo
參數的裝飾器
裝飾器也可作用於方法的入參,這個方法不僅限於類的成員方法,還可以是類的構造器。裝飾器的返回值會被忽略。
當作用於方法的參數時,裝飾器的入參依次為:
- 如果裝飾的是靜態方法則為類的構造器,實例方法則為類的原型。
- 被裝飾的參數名。
- 參數在參數列表中的索引。
比如,定義一個參數為必傳的:
import "reflect-metadata";
const requiredMetadataKey = Symbol("required");
function required(
target: Object,
propertyKey: string | symbol,
parameterIndex: number
) {
let existingRequiredParameters: number[] =
Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
existingRequiredParameters.push(parameterIndex);
Reflect.defineMetadata(
requiredMetadataKey,
existingRequiredParameters,
target,
propertyKey
);
}
function validate(
target: any,
propertyName: string,
descriptor: TypedPropertyDescriptor<Function>
) {
let method = descriptor.value;
descriptor.value = function() {
let requiredParameters: number[] = Reflect.getOwnMetadata(
requiredMetadataKey,
target,
propertyName
);
if (requiredParameters) {
for (let parameterIndex of requiredParameters) {
if (
parameterIndex >= arguments.length ||
arguments[parameterIndex] === undefined
) {
throw new Error("Missing required argument.");
}
}
}
<span class="pl-k">return</span> <span class="pl-smi">method</span>.<span class="pl-c1">apply</span>(<span class="pl-c1">this</span>, <span class="pl-c1">arguments</span>);
};
}
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@validate
greet(@required name: string) {
return "Hello " + name + ", " + this.greeting;
}
}
上面示例中,@required
將參數標記為必需,配合 @validate
在調用真實的方法前進行檢查。