裝飾器是一種特殊的聲明,它可以被附加到類聲明、方法、訪問器、屬性或參數上,用來修改或擴展類的行為。

重要提示:裝飾器目前是一個實驗性特性(截至 TS 5.5)。在 tsconfig.json 中需要啓用 "experimentalDecorators": true。此外,ESNext 的裝飾器提案已進入 Stage 3,語法略有不同,但核心概念相通。


裝飾器執行順序

在深入各個場景之前,先了解執行順序非常重要:

  1. 參數裝飾器 -> 方法裝飾器 -> 訪問器裝飾器 -> 屬性裝飾器 (對於每個實例成員)
  2. 參數裝飾器 -> 方法裝飾器 -> 訪問器裝飾器 -> 屬性裝飾器 (對於每個靜態成員)
  3. 參數裝飾器 (構造函數)
  4. 類裝飾器

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(日誌、權限、性能、事務)、自動綁定 this、防抖/節流

攔截、包裝、替換方法的實現

屬性裝飾器

元數據標記、依賴注入(參數)、響應式數據、訪問監聽

屬性添加元數據或重新定義其 getter/setter

核心價值:裝飾器提供了一種強大的 “元編程” 能力,允許你以聲明式和可複用的方式增強你的代碼,將核心邏輯橫切關注點(如日誌、安全、事務)分離開,使代碼更加清晰和模塊化。它們在 Angular、NestJS、TypeORM 等框架中得到了廣泛應用。