在上一篇文章中,我們初步介紹了裝飾器的基本概念與應用方式。本篇將深入探討 TypeScript 5 之前的舊版裝飾器,並介紹其各種類型。在舊版裝飾器中,有些情況下需要在 tsconfig.json 中額外設定 emitDecoratorMetadata
、experimentalDecorators
來啟用裝飾器元資料。請先將此設定加入,以便順利使用裝飾器功能。
類別裝飾器可以用來修改或擴展類別的定義。它可以用來新增資料、修改原有的類定義或對類進行封裝處理。類別裝飾器會傳原本的類別為參數,並加在要使用此裝飾器的類別上方。
@sealed
@withAge
class Person {
name: string = '阿毛';
}
function sealed<T extends new (...args: any[]) => {}>(baseClass: T) {
console.log('阻止擴展類別裝飾器');
Object.seal(baseClass);
Object.seal(baseClass.prototype);
}
function withAge<T extends new (...args: any[]) => {}>(baseClass: T) {
console.log('套用年齡類別裝飾器');
return class extends baseClass {
age: number = 18;
constructor(...args: any[]) {
super(...args);
console.log(`套用年齡到 ${baseClass.name} 類別`);
}
};
}
如果泛型要約束可實例化類別,可以使用 new (...args: any[]) => {}
來進行約束。
可以看到裝飾器可以套用多個,以上面那個案例,會先執行 withAge
在執行 sealed
,裝飾器會按照從下到上的順序執行,這或許有些反直覺,但如果將裝飾器看作嵌套的函數調用,這個順序就變得合乎邏輯。例如:
// 以下只是幫助想像的範例
sealed(
withAge(
Person
)
)
看到上方範例中使用的 withAge
,是不是覺得寫死年齡是不是不夠彈性,這時候就可以使用裝飾器工廠的撰寫方式,裝飾器工廠允許我們動態生成裝飾器,通過傳遞參數來控制裝飾器的行為。而裝飾器工廠本質上,其實就是一個返回裝飾器函數的函數。
@withAge(18)
class Person {
name: string = '阿毛';
}
function withAge(_age: number) {
return function<T extends new (...args: any[]) => {}>(baseClass: T) {
console.log('套用年齡類別裝飾器');
return class extends baseClass {
age: number = _age;
constructor(...args: any[]) {
super(...args);
console.log(`套用年齡到 ${baseClass.name} 類別`);
}
};
}
}
可以看到我們透過函數的方式更彈性的傳遞參數進去。
方法裝飾器用來修改方法的行為,例如在方法執行前後進行邏輯操作、修改方法的返回值或記錄方法的執行過程。方法裝飾器會傳三個參數,分別為 target
(被裝飾類別的建構子) 、 key
(被裝飾類別的方法名稱)與 descriptor
(被裝飾類別的方法屬性),並加在要使用此裝飾器的類別方法上方。
class Wallet {
private balance: number = 0;
constructor(public limit: number) {}
@limitDeposit
addMoney(amount: number) {
this.balance += amount;
console.log(`已存入 ${amount} 元,目前餘額:${this.balance} 元`);
}
}
// 方法裝飾器來限制單次存款上限
function limitDeposit<T extends { limit: number }>(target: T, key: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(this: T, amount: number) {
if (amount > this.limit) {
console.log(`存款失敗:單次存款不能超過 ${this.limit} 元`);
} else {
originalMethod.apply(this, [amount]);
}
};
}
const myWallet = new Wallet(500); // 設定單次存款上限為 500 元
myWallet.addMoney(100); // 已存入 100 元,目前餘額:100 元
myWallet.addMoney(600); // 存款失敗:單次存款不能超過 500 元
屬性裝飾器可以用來修飾類別的屬性,可以攔截屬性的讀取(getter)和寫入(setter)行為。屬性裝飾器會傳兩個參數,分別為 target
(被裝飾類別的建構子) 與 key
(被裝飾類別的屬性名稱),並加在要使用此裝飾器的類別屬性上方。
class Person {
@watchValue
name: string = '阿毛';
}
function watchValue<T, K extends keyof T & string>(target: T, key: K) {
console.log("屬性裝飾器");
let value = target[key];
const getter = () => value;
const setter = (newValue: T[K]) => {
console.log(`設置 ${key} 屬性值從 ${value} 改為 ${newValue}`);
value = newValue;
};
Object.defineProperty(target, key, {
get: getter,
set: setter,
configurable: true,
enumerable: true,
});
}
const person = new Person();
person.name = "Mao";
參數裝飾器用於修飾方法的參數,這種裝飾器通常用來標註參數或進行參數的驗證。參數裝飾器會傳三個參數,分別為 target
(被裝飾類別的建構子) 、 key
(被裝飾類別的屬性名稱)與 index
(參數的順序),並加在要使用此裝飾器的參數前方。
因為使用情境不多,且 TypeScript 5 把這個裝飾器移除了,所以這邊不用特別放在心上。
class Wallet {
private balance: number = 0;
addMoney(@logParameter amount: number) {
this.balance += amount;
}
}
function logParameter<T>(target: T, key: string, index: number) {
console.log(`${key} 方法的第 ${index + 1} 個參數`);
}
本文介紹了 TypeScript 5 之前的舊版裝飾器,包括類別裝飾器、方法裝飾器、屬性裝飾器、參數裝飾器、裝飾器工廠與裝飾器執行的順序。這些裝飾器提供了一種在不更改原始邏輯的前提下,對類及其成員進行修改和擴展的靈活方式。隨著 TypeScript 5 的更新,裝飾器功能得到了進一步加強,下一篇將介紹 TypeScript 5 的新版裝飾器的使用方式。