隨著TypeScript和ES6里引入了類,現(xiàn)在在一些場景下我們會需要額外的特性,用來支持標(biāo)注或修改類及其成員。 Decorators提供了一種在類的聲明和成員上使用元編程語法添加標(biāo)注的方式。 Javascript里的Decorators目前處在建議征集的第一階段,在TypeScript里做為實驗性特性已經(jīng)提供了支持。
注意 Decorators是實驗性的特性,在未來的版本中可能會發(fā)生改變。
若要啟用實驗性的decorator,你必須啟用experimentalDecorators
編譯器選項,在命令行中或在tsconfig.json
:
命令行:
tsc --target ES5 --experimentalDecorators
tsconfig.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
裝飾器是一種特殊類型的聲明,它能夠被附加到類聲明,方法,訪問符,屬性,或 參數(shù)上。
裝飾器利用@expression
這種方式,expression
求值后必須為一個函數(shù),它使用被裝飾的聲明信息在運行時被調(diào)用。
例如,有一個@sealed
裝飾器,我們會這樣定義sealed
函數(shù):
function sealed(target) {
// do something with "target" ...
}
注意 下面類裝飾器小節(jié)里有一個更加詳細的例子。
如果我們想自定義裝飾器是如何作用于聲明的,我們得寫一個裝飾器工廠函數(shù)。 裝飾器工廠就是一個簡單的函數(shù),它返回一個表達式,以供裝飾器在運行時調(diào)用。
我們可以通過下面的方式來寫一個裝飾器工廠
function color(value: string) { // 這是一個裝飾器工廠
return function (target) { // 這是裝飾器
// do something with "target" and "value"...
}
}
注意 下面方法裝飾器小節(jié)里有一個更加詳細的例子。
多個裝飾器可以同時應(yīng)用到一個聲明上,就像下面的示例:
寫在同一行上:
@f @g x
寫在多行上:
@f
@g
x
當(dāng)多個裝飾器應(yīng)用于一個聲明上,它們求值方式與復(fù)合函數(shù)相似。在這個模型下,當(dāng)復(fù)合f和g時,復(fù)合的結(jié)果(f ° g)(x)等同于f(g(x))。
同樣的,在TypeScript里,當(dāng)多個裝飾器應(yīng)用在一個聲明上時會進行如下步驟的操作:
如果我們使用裝飾器工廠的話,可以通過下面的例子來觀察它們求值的順序:
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() {}
}
在控制臺里會打印出如下結(jié)果:
f(): evaluated
g(): evaluated
g(): called
f(): called
類中不同聲明上的裝飾器將按以下規(guī)定的順序應(yīng)用:
類裝飾器在類聲明之前被聲明(緊貼著類聲明)。
類裝飾器應(yīng)用于類構(gòu)造函數(shù),可以用來監(jiān)視,修改或替換類定義。
類裝飾器不能用在聲明文件中(.d.ts
),也不能用在任何外部上下文中(比如declare
的類)。
類裝飾器表達式會在運行時當(dāng)作函數(shù)被調(diào)用,類的構(gòu)造函數(shù)作為其唯一的參數(shù)。
如果類裝飾器返回一個值,它會使用提供的構(gòu)造函數(shù)來替換類的聲明。
注意 如果你要返回一個新的構(gòu)造函數(shù),你必須注意處理好原來的原型鏈。 在運行時的裝飾器調(diào)用邏輯中不會為你做這些。
下面是使用類裝飾器(@sealed
)的例子,應(yīng)用到Greeter
類:
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
我們可以這樣定義@sealed
裝飾器
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
當(dāng)@sealed
被執(zhí)行的時候,它將密封此類的構(gòu)造函數(shù)和原型。(注:參見Object.seal)
方法裝飾器聲明在一個方法的聲明之前(緊貼著方法聲明)。
它會被應(yīng)用到方法的屬性描述符上,可以用來監(jiān)視,修改或者替換方法定義。
方法裝飾器不能用在聲明文件(.d.ts
),重載或者任何外部上下文(比如declare
的類)中。
方法裝飾器表達式會在運行時當(dāng)作函數(shù)被調(diào)用,傳入下列3個參數(shù):
注意 如果代碼輸出目標(biāo)版本小于
ES5
,Property Descriptor將會是undefined
。
如果方法裝飾器返回一個值,它會被用作方法的屬性描述符。
注意 如果代碼輸出目標(biāo)版本小于
ES5
返回值會被忽略。
下面是一個方法裝飾器(@enumerable
)的例子,應(yīng)用于Greeter
類的方法上:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@enumerable(false)
greet() {
return "Hello, " + this.greeting;
}
}
我們可以用下面的函數(shù)聲明來定義@enumerable
裝飾器:
function enumerable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.enumerable = value;
};
}
這里的@enumerable(false)
是一個裝飾器工廠。
當(dāng)裝飾器@enumerable(false)
被調(diào)用時,它會修改屬性描述符的enumerable
屬性。
訪問符裝飾器聲明在一個訪問符的聲明之前(緊貼著訪問符聲明)。
訪問符裝飾器應(yīng)用于訪問符的屬性描述符并且可以用來監(jiān)視,修改或替換一個訪問符的定義。
訪問符裝飾器不能用在聲明文件中(.d.ts),或者任何外部上下文(比如declare
的類)里。
注意 TypeScript不允許同時裝飾一個成員的
get
和set
訪問符。相反,所有裝飾的成員必須被應(yīng)用到文檔順序指定的第一個訪問符。這是因為,裝飾器應(yīng)用于一個屬性描述符,它聯(lián)合了get
和set
訪問符,而不是分開聲明的。
訪問符裝飾器表達式會在運行時當(dāng)作函數(shù)被調(diào)用,傳入下列3個參數(shù):
注意 如果代碼輸出目標(biāo)版本小于
ES5
,Property Descriptor將會是undefined
。
如果訪問符裝飾器返回一個值,它會被用作方法的屬性描述符。
注意 如果代碼輸出目標(biāo)版本小于
ES5
返回值會被忽略。
下面是使用了訪問符裝飾器(@configurable
)的例子,應(yīng)用于Point
類的成員上:
class Point {
private _x: number;
private _y: number;
constructor(x: number, y: number) {
this._x = x;
this._y = y;
}
@configurable(false)
get x() { return this._x; }
@configurable(false)
get y() { return this._y; }
}
我們可以通過如下函數(shù)聲明來定義@configurable
裝飾器:
function configurable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value;
};
}
屬性裝飾器聲明在一個屬性聲明之前(緊貼著屬性聲明)。
屬性裝飾器不能用在聲明文件中(.d.ts),或者任何外部上下文(比如declare
的類)里。
屬性裝飾器表達式會在運行時當(dāng)作函數(shù)被調(diào)用,傳入下列2個參數(shù):
注意 屬性描述符不會做為參數(shù)傳入屬性裝飾器,這與TypeScript是如何初始化屬性裝飾器的有關(guān)。 因為目前沒有辦法在定義一個原型對象的成員時描述一個實例屬性,并且沒辦法監(jiān)視或修改一個屬性的初始化方法。 因此,屬性描述符只能用來監(jiān)視類中是否聲明了某個名字的屬性。
如果屬性裝飾器返回一個值,它會被用作方法的屬性描述符。
注意 如果代碼輸出目標(biāo)版本小于
ES5
,返回值會被忽略。
如果訪問符裝飾器返回一個值,它會被用作方法的屬性描述符。
我們可以用它來記錄這個屬性的元數(shù)據(jù),如下例所示:
class Greeter {
@format("Hello, %s")
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
let formatString = getFormat(this, "greeting");
return formatString.replace("%s", this.greeting);
}
}
然后定義@format
裝飾器和getFormat
函數(shù):
import "reflect-metadata";
const formatMetadataKey = Symbol("format");
function format(formatString: string) {
return Reflect.metadata(formatMetadataKey, formatString);
}
function getFormat(target: any, propertyKey: string) {
return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}
這個 @format("Hello, %s")
裝飾器是個 裝飾器工廠。
當(dāng)@format("Hello, %s")
被調(diào)用時,它添加一條這個屬性的元數(shù)據(jù),通過reflect-metadata
庫里的Reflect.metadata
函數(shù)。
當(dāng)getFormat
被調(diào)用時,它讀取格式的元數(shù)據(jù)。
注意 這個例子需要使用
reflect-metadata
庫。 查看元數(shù)據(jù)了解reflect-metadata
庫更詳細的信息。
參數(shù)裝飾器聲明在一個參數(shù)聲明之前(緊貼著參數(shù)聲明)。
參數(shù)裝飾器應(yīng)用于類構(gòu)造函數(shù)或方法聲明。
參數(shù)裝飾器不能用在聲明文件(.d.ts),重載或其它外部上下文(比如declare
的類)里。
參數(shù)裝飾器表達式會在運行時當(dāng)作函數(shù)被調(diào)用,傳入下列3個參數(shù):
注意 參數(shù)裝飾器只能用來監(jiān)視一個方法的參數(shù)是否被傳入。
參數(shù)裝飾器的返回值會被忽略。
下例定義了參數(shù)裝飾器(@required
)并應(yīng)用于Greeter
類方法的一個參數(shù):
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@validate
greet(@required name: string) {
return "Hello " + name + ", " + this.greeting;
}
}
然后我們使用下面的函數(shù)定義 @required
和 @validate
裝飾器:
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.");
}
}
}
return method.apply(this, arguments);
}
}
@required
裝飾器添加了元數(shù)據(jù)實體把參數(shù)標(biāo)記為必須的。
@validate
裝飾器把greet
方法包裹在一個函數(shù)里在調(diào)用原先的函數(shù)前驗證函數(shù)參數(shù)。
注意 這個例子使用了
reflect-metadata
庫。 查看元數(shù)據(jù)了解reflect-metadata
庫的更多信息。
一些例子使用了reflect-metadata
庫來支持實驗性的 metadata API。
這個庫還不是ECMAScript (JavaScript)標(biāo)準(zhǔn)的一部分。
然而,當(dāng)裝飾器被ECMAScript官方標(biāo)準(zhǔn)采納后,這些擴展也將被推薦給ECMAScript以采納。
你可以通過npm安裝這個庫:
npm i reflect-metadata --save
TypeScript支持為帶有裝飾器的聲明生成元數(shù)據(jù)。
你需要在命令行或tsconfig.json
里啟用emitDecoratorMetadata
編譯器選項。
Command Line:
tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata
tsconfig.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
當(dāng)啟用后,只要reflect-metadata
庫被引入了,設(shè)計階段額外的信息可以在運行時使用。
如下例所示:
import "reflect-metadata";
class Point {
x: number;
y: number;
}
class Line {
private _p0: Point;
private _p1: Point;
@validate
set p0(value: Point) { this._p0 = value; }
get p0() { return this._p0; }
@validate
set p1(value: Point) { this._p1 = value; }
get p1() { return this._p1; }
}
function validate<T>(target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<T>) {
let set = descriptor.set;
descriptor.set = function (value: T) {
let type = Reflect.getMetadata("design:type", target, propertyKey);
if (!(value instanceof type)) {
throw new TypeError("Invalid type.");
}
}
}
TypeScript編譯器可以通過@Reflect.metadata
裝飾器注入設(shè)計階段的類型信息。
你可以認(rèn)為它相當(dāng)于下面的TypeScript:
class Line {
private _p0: Point;
private _p1: Point;
@validate
@Reflect.metadata("design:type", Point)
set p0(value: Point) { this._p0 = value; }
get p0() { return this._p0; }
@validate
@Reflect.metadata("design:type", Point)
set p1(value: Point) { this._p1 = value; }
get p1() { return this._p1; }
}
注意 裝飾器元數(shù)據(jù)是個實驗性的特性并且可能在以后的版本中發(fā)生破壞性的改變(breaking changes)。