TypeScript 裝飾器實用指南!
一、裝飾器的概念 Summer IS HERE
在 TypeScript 中,裝飾器就是可以添加到類及其成員的函數。TypeScript 裝飾器可以注釋和修改類聲明、方法、屬性和訪問器。Decorator類型定義如下:
type Decorator = (target: Input, context: {
kind: string;
name: string | symbol;
access: {
get?(): unknown;
set?(value: unknown): void;
};
private?: boolean;
static?: boolean;
addInitializer?(initializer: () => void): void;
}) => Output | void;
上面的類型定義解釋如下:
- target:代表要裝飾的元素,其類型為 Input。
- context 包含有關如何聲明修飾方法的元數據,即:
- kind:裝飾值的類型。正如我們將看到的,這可以是類、方法、getter、setter、字段或訪問器。
- name:被裝飾對象的名稱。
- access:引用 getter 和 setter 方法來訪問裝飾對象的對象。
- private:被裝飾的對象是否是私有類成員。
- static:被修飾的對象是否是靜態類成員。
- addInitializer:一種在構造函數開頭(或定義類時)添加自定義初始化邏輯的方法。
- Output:表示 Decorator 函數返回值的類型。
二、裝飾器的類型 Summer IS HERE
接下來,我們就來了解一下裝飾器的各種類型。
Summer:類裝飾器
當將函數作為裝飾器附加到類時,將收到類構造函數作為第一個參數:
type ClassDecorator = (value: Function, context: {
kind: "class"
name: string | undefined
addInitializer(initializer: () => void): void
}) => Function | void
例如,假設想要使用裝飾器向 Rocket 類添加兩個屬性:fuel 和 isEmpty()。在這種情況下,可以編寫以下函數:
function WithFuel(target: typeof Rocket, context): typeof Rocket {
if (context.kind === "class") {
return class extends target {
fuel: number = 50
isEmpty(): boolean {
return this.fuel == 0
}
}
}
}
在確保裝飾元素的類型確實是類之后,返回一個具有兩個附加屬性的新類。或者,可以使用原型對象來動態添加新方法:
function WithFuel(target: typeof Rocket, context): typeof Rocket {
if (context.kind === "class") {
target.prototype.fuel = 50
target.prototype.isEmpty = (): boolean => {
return this.fuel == 0
}
}
}
可以按以下方式使用 WithFuel:
@WithFuel
class Rocket {}
const rocket = new Rocket()
console.log((rocket as any).fuel)
console.log(`empty? ${(rocket as any).isEmpty()}`)
/* Prints:
50
empty? false
*/
可以看到,這里將rocket轉換為any類型才能訪問新的屬性。這是因為裝飾器無法影響類型的結構。
如果原始類定義了一個稍后被裝飾的屬性,裝飾器會覆蓋原始值。例如,如果Rocket有一個具有不同值的fuel屬性,WithFuel裝飾器將會覆蓋該值:
function WithFuel(target: typeof Rocket, context): typeof Rocket {
if (context.kind === "class") {
return class extends target {
fuel: number = 50
isEmpty(): boolean {
return this.fuel == 0
}
}
}
}
@WithFuel
class Rocket {
fuel: number = 75
}
const rocket = new Rocket()
console.log((rocket as any).fuel)
// 50
Summer:方法裝飾器
方法裝飾器可以用于裝飾類方法。在這種情況下,裝飾器函數的類型如下:
type ClassMethodDecorator = (target: Function, context: {
kind: "method"
name: string | symbol
access: { get(): unknown }
static: boolean
private: boolean
addInitializer(initializer: () => void): void
}) => Function | void
如果希望在調用被裝飾的方法之前或之后執行某些操作時,就可以使用方法裝飾器。
例如,在開發過程中,記錄對特定方法的調用或在調用之前/之后驗證前置/后置條件可能非常有用。此外,我們還可以影響方法的調用方式,例如通過延遲其執行或限制在一定時間內的調用次數。
最后,可以使用方法裝飾器將一個方法標記為已廢棄,并記錄一條消息來警告用戶,并告知他們應該使用哪個方法代替:
function deprecatedMethod(target: Function, context) {
if (context.kind === "method") {
return function (...args: any[]) {
console.log(`${context.name} is deprecated and will be removed in a future version.`)
return target.apply(this, args)
}
}
}
在這種情況下,deprecatedMethod函數的第一個參數是要裝飾的方法。確認它確實是一個方法后(context.kind === "method"),返回一個新的函數,該函數在調用實際方法之前包裝被裝飾的方法并記錄一條警告消息。
接下來,可以按照以下方式使用裝飾器:
@WithFuel
class Rocket {
fuel: number = 75
@deprecatedMethod
isReadyForLaunch(): Boolean {
return !(this as any).isEmpty()
}
}
const rocket = new Rocket()
console.log(`Is ready for launch? ${rocket.isReadyForLaunch()}`)
在isReadyForLaunch()方法中,引用了通過WithFuel裝飾器添加的isEmpty方法。注意,必須將其轉換為any類型的實例,與之前一樣。當調用isReadyForLaunch()方法時,會看到以下輸出,顯示警告消息被正確地打印出來:
isReadyForLaunch is deprecated and will be removed in a future version.
Is the ready for launch? true
Summer:屬性裝飾器
屬性裝飾器與方法裝飾器的類型非常相似:
type ClassPropertyDecorator = (target: undefined, context: {
kind: "field"
name: string | symbol
access: { get(): unknown, set(value: unknown): void }
static: boolean
private: boolean
}) => (initialValue: unknown) => unknown | void
屬性裝飾器的用例與方法裝飾器的用法也非常相似。例如,可以跟蹤對屬性的訪問或將其標記為已棄用:
function deprecatedProperty(_: any, context) {
if (context.kind === "field") {
return function (initialValue: any) {
console.log(`${context.name} is deprecated and will be removed in a future version.`)
return initialValue
}
}
}
代碼與為方法定義的 deprecatedMethod 裝飾器非常相似,它的用法也是如此。
Summer:訪問器裝飾器
與方法裝飾器非常相似的是訪問器裝飾器,它是針對 getter 和 setter 的裝飾器:
type ClassSetterDecorator = (target: Function, context: {
kind: "setter"
name: string | symbol
access: { set(value: unknown): void }
static: boolean
private: boolean
addInitializer(initializer: () => void): void
}) => Function | void
type ClassGetterDecorator = (value: Function, context: {
kind: "getter"
name: string | symbol
access: { get(): unknown }
static: boolean
private: boolean
addInitializer(initializer: () => void): void
}) => Function | void
訪問器裝飾器的定義與方法裝飾器的定義類似。例如,可以將 deprecatedMethod 和 deprecatedProperty 修飾合并到一個已棄用的函數中,該函數也支持 getter 和 setter:
function deprecated(target, context) {
const kind = context.kind
const msg = `${context.name} is deprecated and will be removed in a future version.`
if (kind === "method" || kind === "getter" || kind === "setter") {
return function (...args: any[]) {
console.log(msg)
return target.apply(this, args)
}
} else if (kind === "field") {
return function (initialValue: any) {
console.log(msg)
return initialValue
}
}
}
三、裝飾器的用例 Summer IS HERE
上面介紹了裝飾器是什么以及如何正確使用它們,下面來看看裝飾器可以幫助我們解決的一些具體問題。
Summer:計算執行時間
假設想要估計運行一個函數需要多長時間,以此來衡量應用的性能??梢詣摻ㄒ粋€裝飾器來計算方法的執行時間并將其打印在控制臺上:
class Rocket {
@measure
launch() {
console.log("3... 2... 1... ??");
}
}
Rocket 類內部有一個 launch方法。要測量launch方法的執行時間,可以附加measure 裝飾器:
import { performance } from "perf_hooks";
function measure(target: Function, context) {
if (context.kind === "method") {
return function (...args: any[]) {
const start = performance.now()
const result = target.apply(this, args)
const end = performance.now()
console.log(`Time: ${end - start} s`)
return result
}
}
}
可以看到,measure裝飾器會替換原始方法,并使用新方法來計算原始方法的執行時間并將其打印到控制臺。為了計算執行時間,可以使用 Node.js 標準庫中的性能鉤子(Performance Hooks)API。實例化一個新的Rocket對象并調用launch方法:
const rocket = new Rocket()
rocket.launch()
將得到以下結果:
3... 2... 1... ??
Time: 1.062355000525713 s
Summer:使用裝飾器工廠函數
要將裝飾器配置為在特定場景中采取不同的行為,可以使用裝飾器工廠。裝飾器工廠是返回裝飾器的函數。這樣就能夠通過在工廠中傳遞一些參數來自定義裝飾器的行為。
來看下面的例子:
function fill(value: number) {
return function(_, context) {
if (context.kind === "field") {
return function (initialValue: number) {
return value + initialValue
}
}
}
}
fill 函數返回一個裝飾器,根據從工廠傳入的值來改變屬性的值:
class Rocket {
@fill(20)
fuel: number = 50
}
const rocket = new Rocket()
console.log(rocket.fuel) // 70
Summer:自動錯誤攔截
裝飾器的另一個常見用例是檢查方法調用的前置條件和后置條件。例如,假設要在調用 launch()
方法之前確保 Fuel
至少為給定值:
class Rocket {
fuel = 50
launch() {
console.log("3... 2... 1... ??")
}
}
假設有一個 Rocket 類,它有一個 launchToMars 方法。要發射火箭,燃料(fuel)必須高于一個值,例如 75。
下面來為它創建裝飾器:
function minimumFuel(fuel: number) {
return function(target: Function, context) {
if (context.kind === "method") {
return function (...args: any[]) {
if (this.fuel > fuel) {
return target.apply(this, args)
} else {
console.log(`Not enough fuel. Required: ${fuel}, got ${this.fuel}`)
}
}
}
}
}
minimumFuel是一個工廠裝飾器。它接受一個 fuel 參數,表示啟動特定火箭所需的燃料量。為了檢查燃料條件,將原始方法包裹在一個新方法中。注意,在運行時可以自由地引用 this.fuel。
現在就可以將裝飾器應用到launch方法上,并設置最低燃料量:
class Rocket {
fuel = 50
@minimumFuel(75)
launch() {
console.log("3... 2... 1... ??")
}
}
如果現在調用 launch 方法,它不會發射火箭,因為當前的燃料量為 50:
const rocket = new Rocket()
rocket.launch()
Not enough fuel. Required: 75, got 50
[1]裝飾器提案: https://github.com/tc39/proposal-decorators。