裝飾器是一種特殊的聲明,它可以被附加到類聲明、方法、訪問器、屬性或參數上,用來修改或擴展類的行為。
重要提示:裝飾器目前是一個實驗性特性(截至 TS 5.5)。在
tsconfig.json中需要啓用"experimentalDecorators": true。此外,ESNext 的裝飾器提案已進入 Stage 3,語法略有不同,但核心概念相通。
裝飾器執行順序
在深入各個場景之前,先了解執行順序非常重要:
- 參數裝飾器 -> 方法裝飾器 -> 訪問器裝飾器 -> 屬性裝飾器 (對於每個實例成員)
- 參數裝飾器 -> 方法裝飾器 -> 訪問器裝飾器 -> 屬性裝飾器 (對於每個靜態成員)
- 參數裝飾器 (構造函數)
- 類裝飾器
1. 類裝飾器 (Class Decorator)
應用場景:應用於類構造函數,用於觀察、修改或替換類定義。常用於依賴注入、組件註冊、日誌、給類添加元數據等。
簽名:
declare type ClassDecorator = <TFunction extends Function>(
target: TFunction // 類的構造函數
) => TFunction | void;
示例 1:日誌(觀察類)
// 一個簡單的日誌裝飾器,在類被實例化時打印日誌
function LogClass(target: Function) {
console.log(`Class ${target.name} was defined.`);
}
@LogClass
class MyService {
// ...
}
// 輸出: "Class MyService was defined."
示例 2:混入/擴展(修改類)
// 一個裝飾器,將新的方法混入到類中
function AddTimestamp<T extends { new (...args: any[]): {} }>(constructor: T) {
return class extends constructor {
createdAt = new Date(); // 為每個實例添加一個新的屬性
};
}
@AddTimestamp
class Document {
title: string;
constructor(title: string) {
this.title = title;
}
}
const doc = new Document('My Report');
console.log(doc.createdAt); // 輸出當前時間,這個屬性是裝飾器添加的
示例 3:依賴注入/註冊(替換類)
// 模擬一個簡單的服務註冊表
const serviceRegistry = new Map();
function Injectable(token: string) {
return function (target: Function) {
// 將類的構造函數註冊到容器中
serviceRegistry.set(token, target);
};
}
@Injectable('UserService')
class UserService {
getUsers() {
return ['Alice', 'Bob'];
}
}
// 其他地方可以通過 token 獲取類的實例
const UserServiceConstructor = serviceRegistry.get('UserService');
const userServiceInstance = new UserServiceConstructor();
2. 方法裝飾器 (Method Decorator)
應用場景:應用於類的方法上。這是最常用的裝飾器類型,用於方法攔截、權限控制、日誌、性能監控、事務管理、防抖/節流等。
簽名:
declare type MethodDecorator = <T>(
target: Object, // 類的原型(對於實例方法)或類本身(對於靜態方法)
propertyKey: string | symbol, // 方法名
descriptor: TypedPropertyDescriptor<T> // 屬性描述符(包含value, writable等)
) => TypedPropertyDescriptor<T> | void;
示例 1:性能監控(計算執行時間)
function MeasureTime(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value; // 保存原始方法
// 重寫該方法
descriptor.value = function (...args: any[]) {
const start = performance.now();
const result = originalMethod.apply(this, args); // 執行原始方法
const end = performance.now();
console.log(`方法 ${propertyKey} 執行耗時: ${(end - start).toFixed(2)} 毫秒`);
return result;
};
return descriptor;
}
class DataProcessor {
@MeasureTime
processLargeData() {
// 模擬耗時操作
for (let i = 0; i < 1e7; i++) {}
console.log('數據處理完成');
}
}
const processor = new DataProcessor();
processor.processLargeData();
// 輸出:
// "數據處理完成"
// "方法 processLargeData 執行耗時: 12.34 毫秒"
示例 2:權限控制(AOP - 面向切面編程)
function AdminRequired(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
// 模擬檢查用户角色(在實際應用中,這可能來自請求上下文)
const userRole = 'user'; // 嘗試改為 'admin' 看看效果
if (userRole !== 'admin') {
throw new Error('無權執行此操作!需要管理員權限。');
}
return originalMethod.apply(this, args);
};
return descriptor;
}
class UserController {
@AdminRequired
deleteUser(userId: number) {
console.log(`用户 ${userId} 已被刪除`);
}
}
const controller = new UserController();
controller.deleteUser(123); // 拋出錯誤: "無權執行此操作!需要管理員權限。"
示例 3:自動綁定 this(解決 this 指向問題)
function AutoBind(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
// 調整屬性描述符,使用getter返回一個已綁定的函數
return {
configurable: true,
enumerable: false,
get() {
// 當訪問這個屬性時,返回一個已經綁定了當前實例的原始方法
const boundFn = originalMethod.bind(this);
return boundFn;
},
};
}
class Printer {
message = 'Hello, world!';
@AutoBind
showMessage() {
console.log(this.message);
}
}
const p = new Printer();
const button = document.createElement('button');
button.textContent = 'Click Me';
// 直接將方法引用作為回調,無需再寫 .bind(p)
button.addEventListener('click', p.showMessage);
document.body.appendChild(button);
// 點擊按鈕會正確輸出 "Hello, world!"
3. 屬性裝飾器 (Property Decorator)
應用場景:應用於類的屬性上。它不能直接修改屬性的值,但常用於添加元數據、依賴注入(如 Angular)、監聽屬性變化、實現響應式數據等。
簽名:
declare type PropertyDecorator = (
target: Object, // 類的原型(對於實例屬性)或類本身(對於靜態屬性)
propertyKey: string | symbol // 屬性名
) => void;
示例 1:元數據反射(常用於依賴注入框架)
import 'reflect-metadata'; // 一個提供反射API的庫
function Format(formatString: string) {
return function (target: any, propertyKey: string) {
// 將格式信息作為元數據附加到屬性上
Reflect.defineMetadata('format', formatString, target, propertyKey);
};
}
function LogProperty(target: any, propertyKey: string) {
// 監聽屬性訪問(這是一個簡單示例,實際實現更復雜)
let value: any;
const getter = function () {
console.log(`獲取 ${propertyKey}: ${value}`);
return value;
};
const setter = function (newVal: any) {
console.log(`設置 ${propertyKey} 為: ${newVal}`);
value = newVal;
};
// 在原型上重新定義該屬性,使用新的 getter 和 setter
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
}
class Person {
@LogProperty
@Format('YYYY-MM-DD')
birthday: string;
constructor(birthday: string) {
this.birthday = birthday;
}
getBirthdayFormat() {
// 可以獲取存儲在元數據中的格式
const format = Reflect.getMetadata('format', this, 'birthday');
return format; // 返回 'YYYY-MM-DD'
}
}
const p = new Person('1990-01-01');
// 輸出: "設置 birthday 為: 1990-01-01"
p.birthday = '2000-12-31';
// 輸出: "設置 birthday 為: 2000-12-31"
const b = p.birthday;
// 輸出: "獲取 birthday: 2000-12-31"
console.log(p.getBirthdayFormat()); // 輸出: "YYYY-MM-DD"
示例 2:簡易響應式(Vue 2 風格的原理)
// 一個非常簡化的響應式系統
const dep = new Set(); // 依賴收集器
function Observable(target: any, propertyKey: string) {
let value = target[propertyKey];
const getter = function () {
// 收集依賴(例如,當前的渲染函數)
dep.add('someEffect');
return value;
};
const setter = function (newVal: any) {
if (value !== newVal) {
value = newVal;
console.log(`屬性 ${propertyKey} 變化了,觸發更新!`);
// 通知所有依賴進行更新
dep.forEach(effect => {
console.log(`執行效果: ${effect}`);
// 在實際框架中,這裏會調用 effect() 來重新渲染或計算
});
}
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
}
class Store {
@Observable
count = 0;
}
const store = new Store();
console.log(store.count); // 收集依賴
store.count = 42; // 輸出: "屬性 count 變化了,觸發更新!" 和 "執行效果: someEffect"
總結
|
裝飾器類型 |
核心應用場景 |
關鍵能力 |
|
類裝飾器 |
依賴注入、組件註冊、全局配置、混入功能 |
觀察、修改或替換類構造函數 |
|
方法裝飾器 |
AOP(日誌、權限、性能、事務)、自動綁定 |
攔截、包裝、替換方法的實現 |
|
屬性裝飾器 |
元數據標記、依賴注入(參數)、響應式數據、訪問監聽 |
為屬性添加元數據或重新定義其 getter/setter |
核心價值:裝飾器提供了一種強大的 “元編程” 能力,允許你以聲明式和可複用的方式增強你的代碼,將核心邏輯與橫切關注點(如日誌、安全、事務)分離開,使代碼更加清晰和模塊化。它們在 Angular、NestJS、TypeORM 等框架中得到了廣泛應用。