注意:本文件涉及實驗性的第 2 階段(Stage 2)裝飾器實現。TypeScript 5.0 起支援第 3 階段(Stage 3)裝飾器。請參閱:TypeScript 5.0 中的裝飾器
介紹
隨著 TypeScript 和 ES6 中類的引入,現在存在某些需要附加功能來支援註釋或修改類及類成員的場景。裝飾器提供了一種為類宣告和成員添加註釋和超程式設計語法的方法。
延伸閱讀(第 2 階段):TypeScript 裝飾器完全指南
要啟用對裝飾器的實驗性支援,您必須在命令列或 tsconfig.json 中啟用 experimentalDecorators 編譯器選項。
命令列:
shelltsc --target ES5 --experimentalDecorators
tsconfig.json:
{"": {"": "ES5","": true}}
裝飾器
裝飾器是一種特殊型別的宣告,可以附加到類宣告、方法、訪問器、屬性或引數上。裝飾器使用 @expression 形式,其中 expression 必須求值為一個函式,該函式會在執行時被呼叫,並帶有關於被裝飾宣告的資訊。
例如,給定 @sealed 裝飾器,我們可以按照以下方式編寫 sealed 函式:
tsfunction sealed(target) {// do something with 'target' ...}
裝飾器工廠
如果我們想要自定義裝飾器如何應用於宣告,我們可以編寫一個裝飾器工廠。裝飾器工廠只是一個返回表示式的函式,該表示式將在執行時由裝飾器呼叫。
我們可以透過以下方式編寫裝飾器工廠:
tsfunction color(value: string) {// this is the decorator factory, it sets up// the returned decorator functionreturn function (target) {// this is the decorator// do something with 'target' and 'value'...};}
裝飾器組合
可以在一個宣告上應用多個裝飾器,例如在同一行:
tsTry@f @g x
在多行上:
tsTry@f @g x
當多個裝飾器應用於單個宣告時,它們的求值類似於數學中的函式組合。在這種模型中,當組合函式 f 和 g 時,得到的組合 (f ∘ g)(x) 等價於 f(g(x))。
因此,在 TypeScript 中對單個宣告求值多個裝飾器時,會執行以下步驟:
- 每個裝飾器的表示式按從上到下的順序求值。
- 然後,求值結果按從下到上的順序作為函式呼叫。
如果我們使用裝飾器工廠,我們可以透過以下示例觀察此求值順序:
tsTryfunctionfirst () {console .log ("first(): factory evaluated");return function (target : any,propertyKey : string,descriptor :PropertyDescriptor ) {console .log ("first(): called");};}functionsecond () {console .log ("second(): factory evaluated");return function (target : any,propertyKey : string,descriptor :PropertyDescriptor ) {console .log ("second(): called");};}classExampleClass {@first ()@second ()method () {}}
這會在控制檯中列印以下輸出:
shellfirst(): factory evaluatedsecond(): factory evaluatedsecond(): calledfirst(): called
裝飾器求值
類內部應用到各種宣告的裝飾器具有定義明確的應用順序:
- 引數裝飾器,然後是每個例項成員的方法、訪問器或屬性裝飾器。
- 引數裝飾器,然後是每個靜態成員的方法、訪問器或屬性裝飾器。
- 引數裝飾器應用於建構函式。
- 類裝飾器應用於類。
類裝飾器
類裝飾器在類宣告之前宣告。類裝飾器應用於類的建構函式,可以用來監視、修改或替換類定義。類裝飾器不能在宣告檔案或任何其他環境上下文(例如 declare 類)中使用。
類裝飾器的表示式將在執行時作為函式呼叫,並將被裝飾類的建構函式作為其唯一引數。
如果類裝飾器返回一個值,它將使用提供的建構函式替換類宣告。
注意:如果您選擇返回一個新的建構函式,必須小心維護原始原型。在執行時應用裝飾器的邏輯不會為您自動處理。
以下是將類裝飾器 (@sealed) 應用於 BugReport 類的示例:
tsTry@sealed classBugReport {type = "report";title : string;constructor(t : string) {this.title =t ;}}
我們可以使用以下函式宣告定義 @sealed 裝飾器:
tsfunction sealed(constructor: Function) {Object.seal(constructor);Object.seal(constructor.prototype);}
當 @sealed 執行時,它會密封建構函式及其原型,因此在執行時透過訪問 BugReport.prototype 或在 BugReport 本身上定義屬性,將無法再向該類新增或刪除任何功能(請注意,ES2015 類實際上只是基於原型的建構函式的語法糖)。此裝飾器不會阻止類繼承 BugReport。
接下來是一個如何覆蓋建構函式以設定新預設值的示例。
tsTryfunctionreportableClassDecorator <T extends { new (...args : any[]): {} }>(constructor :T ) {return class extendsconstructor {reportingURL = "http://www...";};}@reportableClassDecorator classBugReport {type = "report";title : string;constructor(t : string) {this.title =t ;}}constbug = newBugReport ("Needs dark mode");console .log (bug .title ); // Prints "Needs dark mode"console .log (bug .type ); // Prints "report"// Note that the decorator _does not_ change the TypeScript type// and so the new property `reportingURL` is not known// to the type system:Property 'reportingURL' does not exist on type 'BugReport'.2339Property 'reportingURL' does not exist on type 'BugReport'.bug .; reportingURL
方法裝飾器
方法裝飾器在方法宣告之前宣告。裝飾器應用於方法的屬性描述符,可以用來監視、修改或替換方法定義。方法裝飾器不能在宣告檔案、過載或任何其他環境上下文(例如 declare 類)中使用。
方法裝飾器的表示式將在執行時作為函式呼叫,帶有以下三個引數:
- 對於靜態成員,是類的建構函式;對於例項成員,是類的原型。
- 成員的名稱。
- 成員的屬性描述符。
注意:如果您的指令碼目標低於
ES5,屬性描述符將為undefined。
如果方法裝飾器返回一個值,它將被用作該方法的屬性描述符。
注意:如果您的指令碼目標低於
ES5,返回值將被忽略。
以下是將方法裝飾器 (@enumerable) 應用於 Greeter 類方法的示例:
tsTryclassGreeter {greeting : string;constructor(message : string) {this.greeting =message ;}@enumerable (false)greet () {return "Hello, " + this.greeting ;}}
我們可以使用以下函式宣告定義 @enumerable 裝飾器:
tsTryfunctionenumerable (value : boolean) {return function (target : any,propertyKey : string,descriptor :PropertyDescriptor ) {descriptor .enumerable =value ;};}
這裡的 @enumerable(false) 裝飾器是一個裝飾器工廠。當呼叫 @enumerable(false) 時,它會修改屬性描述符的 enumerable 屬性。
訪問器裝飾器
訪問器裝飾器在訪問器宣告之前宣告。訪問器裝飾器應用於訪問器的屬性描述符,可以用來監視、修改或替換訪問器的定義。訪問器裝飾器不能在宣告檔案或任何其他環境上下文(例如 declare 類)中使用。
注意:TypeScript 不允許為單個成員同時裝飾
get和set訪問器。相反,該成員的所有裝飾器必須應用到文件順序中指定的第一個訪問器上。這是因為裝飾器應用於屬性描述符,它結合了get和set訪問器,而不是分別裝飾每個宣告。
訪問器裝飾器的表示式將在執行時作為函式呼叫,帶有以下三個引數:
- 對於靜態成員,是類的建構函式;對於例項成員,是類的原型。
- 成員的名稱。
- 成員的屬性描述符。
注意:如果您的指令碼目標低於
ES5,屬性描述符將為undefined。
如果訪問器裝飾器返回一個值,它將被用作該成員的屬性描述符。
注意:如果您的指令碼目標低於
ES5,返回值將被忽略。
以下是將訪問器裝飾器 (@configurable) 應用於 Point 類成員的示例:
tsTryclassPoint {private_x : number;private_y : number;constructor(x : number,y : number) {this._x =x ;this._y =y ;}@configurable (false)getx () {return this._x ;}@configurable (false)gety () {return this._y ;}}
我們可以使用以下函式宣告定義 @configurable 裝飾器:
tsfunction configurable(value: boolean) {return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {descriptor.configurable = value;};}
屬性裝飾器
屬性裝飾器在屬性宣告之前宣告。屬性裝飾器不能在宣告檔案或任何其他環境上下文(例如 declare 類)中使用。
屬性裝飾器的表示式將在執行時作為函式呼叫,帶有以下兩個引數:
- 對於靜態成員,是類的建構函式;對於例項成員,是類的原型。
- 成員的名稱。
注意:由於 TypeScript 初始化屬性裝飾器的方式,屬性裝飾器不會提供屬性描述符作為引數。這是因為目前還沒有機制可以在定義原型成員時描述例項屬性,也沒有辦法監視或修改屬性的初始化程式。返回值也會被忽略。因此,屬性裝飾器只能用於監視類是否聲明瞭特定名稱的屬性。
我們可以利用這些資訊記錄關於屬性的元資料,如下例所示:
tsclass 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 函式:
tsimport "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") 裝飾器是一個裝飾器工廠。當呼叫 @format("Hello, %s") 時,它會使用 reflect-metadata 庫中的 Reflect.metadata 函式為屬性新增元資料條目。當呼叫 getFormat 時,它會讀取格式的元資料值。
注意:此示例需要
reflect-metadata庫。有關reflect-metadata庫的更多資訊,請參閱元資料。
引數裝飾器
引數裝飾器在引數宣告之前宣告。引數裝飾器應用於類建構函式或方法宣告的函式。引數裝飾器不能在宣告檔案、過載或任何其他環境上下文(例如 declare 類)中使用。
引數裝飾器的表示式將在執行時作為函式呼叫,帶有以下三個引數:
- 對於靜態成員,是類的建構函式;對於例項成員,是類的原型。
- 成員的名稱。
- 函式引數列表中引數的序號索引。
注意:引數裝飾器只能用於監視方法上是否聲明瞭引數。
引數裝飾器的返回值將被忽略。
以下是將引數裝飾器 (@required) 應用於 BugReport 類成員引數的示例:
tsTryclassBugReport {type = "report";title : string;constructor(t : string) {this.title =t ;}@validate required verbose : boolean) {if (verbose ) {return `type: ${this.type }\ntitle: ${this.title }`;} else {return this.title ;}}}
然後,我們可以使用以下函式宣告定義 @required 和 @validate 裝飾器:
tsTryimport "reflect-metadata";constrequiredMetadataKey =Symbol ("required");functionrequired (target :Object ,propertyKey : string | symbol,parameterIndex : number) {letexistingRequiredParameters : number[] =Reflect .getOwnMetadata (requiredMetadataKey ,target ,propertyKey ) || [];existingRequiredParameters .push (parameterIndex );Reflect .defineMetadata (requiredMetadataKey ,existingRequiredParameters ,target ,propertyKey );}functionvalidate (target : any,propertyName : string,descriptor :TypedPropertyDescriptor <Function >) {letmethod =descriptor .value !;descriptor .value = function () {letrequiredParameters : number[] =Reflect .getOwnMetadata (requiredMetadataKey ,target ,propertyName );if (requiredParameters ) {for (letparameterIndex ofrequiredParameters ) {if (parameterIndex >=arguments .length ||arguments [parameterIndex ] ===undefined ) {throw newError ("Missing required argument.");}}}returnmethod .apply (this,arguments );};}
@required 裝飾器新增一個元資料條目,將該引數標記為必需。然後,@validate 裝飾器將現有的 print 方法包裝在一個函式中,該函式在呼叫原始方法之前驗證引數。
注意:此示例需要
reflect-metadata庫。有關reflect-metadata庫的更多資訊,請參閱元資料。
元資料
一些示例使用了 reflect-metadata 庫,它為實驗性元資料 API 添加了一個 polyfill。此庫尚不是 ECMAScript (JavaScript) 標準的一部分。然而,一旦裝飾器被正式採納為 ECMAScript 標準的一部分,這些擴充套件將被提議採納。
您可以透過 npm 安裝此庫:
shellnpm i reflect-metadata --save
TypeScript 包含對為具有裝飾器的聲明發出某些型別元資料的實驗性支援。要啟用此實驗性支援,您必須在命令列或 tsconfig.json 中設定 emitDecoratorMetadata 編譯器選項。
命令列:
shelltsc --target ES5 --experimentalDecorators --emitDecoratorMetadata
tsconfig.json:
{"": {"": "ES5","": true,"": true}}
啟用後,只要匯入了 reflect-metadata 庫,額外的設計時型別資訊就會在執行時暴露出來。
我們可以在以下示例中看到其實際效果:
tsTryimport "reflect-metadata";classPoint {constructor(publicx : number, publicy : number) {}}classLine {private_start :Point ;private_end :Point ;@validate setstart (value :Point ) {this._start =value ;}getstart () {return this._start ;}@validate setend (value :Point ) {this._end =value ;}getend () {return this._end ;}}functionvalidate <T >(target : any,propertyKey : string,descriptor :TypedPropertyDescriptor <T >) {letset =descriptor .set !;descriptor .set = function (value :T ) {lettype =Reflect .getMetadata ("design:type",target ,propertyKey );if (!(value instanceoftype )) {throw newTypeError (`Invalid type, got ${typeofvalue } not ${type .name }.`);}set .call (this,value );};}constline = newLine ()line .start = newPoint (0, 0)// @ts-ignore// line.end = {}// Fails at runtime with:// > Invalid type, got object not Point
TypeScript 編譯器將使用 @Reflect.metadata 裝飾器注入設計時型別資訊。您可以將其視為等同於以下 TypeScript 程式碼:
tsclass Line {private _start: Point;private _end: Point;@validate@Reflect.metadata("design:type", Point)set start(value: Point) {this._start = value;}get start() {return this._start;}@validate@Reflect.metadata("design:type", Point)set end(value: Point) {this._end = value;}get end() {return this._end;}}
注意:裝飾器元資料是一項實驗性功能,未來版本中可能會引入重大變更。