Skip to content

类装饰器详解

概述

在 TypeScript 中,装饰器可以应用于类的不同部分:类本身、类的方法、类的属性以及方法的参数。每种装饰器都有其特定的应用场景和实现方式。本文将深入探讨各种装饰器的详细用法,帮助你掌握装饰器的强大功能。

先修知识

在学习本文档之前,建议先了解:

类装饰器(Class Decorator)

类装饰器应用于类声明,可以观察、修改或替换类定义。类装饰器在类定义时执行,而不是在实例化时执行。

函数签名

typescript
type ClassDecorator = <TFunction extends Function>(
  target: TFunction
) => TFunction | void;

参数说明

  • target: 被装饰的类的构造函数

返回值

  • 如果返回一个构造函数,将替换原来的类
  • 如果返回 voidundefined,则保持原类不变

基本用法

typescript
// 简单的类装饰器:添加元数据
function addMetadata(target: Function) {
  (target as any).metadata = {
    version: "1.0.0",
    author: "TypeScript Team"
  };
}

@addMetadata
class MyClass {
  // ...
}

console.log((MyClass as any).metadata);
// { version: "1.0.0", author: "TypeScript Team" }

修改类定义

类装饰器可以返回一个新的构造函数来扩展或替换原来的类:

typescript
// 添加实例方法的装饰器
function addMethod(methodName: string, method: Function) {
  return function <T extends { new (...args: any[]): {} }>(target: T) {
    return class extends target {
      [methodName] = method;
    };
  };
}

@addMethod("greet", function() { return "Hello!"; })
class Person {
  name: string;
  
  constructor(name: string) {
    this.name = name;
  }
}

const person = new Person("John");
console.log((person as any).greet()); // "Hello!"

实际应用:单例模式

typescript
// 单例装饰器:确保类只有一个实例
function singleton<T extends { new (...args: any[]): {} }>(target: T) {
  let instance: InstanceType<T>;
  
  return class extends target {
    constructor(...args: any[]) {
      if (instance) {
        return instance;
      }
      super(...args);
      instance = this as InstanceType<T>;
    }
  };
}

@singleton
class DatabaseConnection {
  private connection: string;
  
  constructor() {
    this.connection = "Connected to database";
    console.log("创建数据库连接");
  }
  
  query(sql: string) {
    return `执行查询: ${sql}`;
  }
}

const db1 = new DatabaseConnection(); // 输出: "创建数据库连接"
const db2 = new DatabaseConnection(); // 不输出,返回已有实例
console.log(db1 === db2); // true

实际应用:依赖注入容器

typescript
// 注册服务到容器的装饰器
const container = new Map<string, any>();

function Injectable(token?: string) {
  return function <T extends { new (...args: any[]): {} }>(target: T) {
    const serviceToken = token || target.name;
    container.set(serviceToken, target);
    return target;
  };
}

@Injectable("UserService")
class UserService {
  getUsers() {
    return ["Alice", "Bob", "Charlie"];
  }
}

@Injectable("ProductService")
class ProductService {
  getProducts() {
    return ["Laptop", "Phone", "Tablet"];
  }
}

// 从容器获取服务
const UserServiceClass = container.get("UserService");
const userService = new UserServiceClass();
console.log(userService.getUsers()); // ["Alice", "Bob", "Charlie"]

实际应用:类验证装饰器

typescript
// 验证类实例的装饰器
function validateClass(validator: (instance: any) => boolean) {
  return function <T extends { new (...args: any[]): {} }>(target: T) {
    return class extends target {
      constructor(...args: any[]) {
        super(...args);
        if (!validator(this)) {
          throw new Error(`验证失败: ${target.name}`);
        }
      }
    };
  };
}

@validateClass((instance: any) => instance.name && instance.age > 0)
class Person {
  name: string;
  age: number;
  
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

const person1 = new Person("John", 30); // ✅ 成功
// const person2 = new Person("", 30);   // ❌ 错误: 验证失败
// const person3 = new Person("John", -1); // ❌ 错误: 验证失败

方法装饰器(Method Decorator)

方法装饰器应用于类的方法,可以观察、修改或替换方法定义。方法装饰器在方法定义时执行。

函数签名

typescript
type MethodDecorator = <T>(
  target: any,
  propertyKey: string | symbol,
  descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;

参数说明

  • target: 类的原型对象(对于实例方法)或类构造函数(对于静态方法)
  • propertyKey: 方法名称
  • descriptor: 属性描述符,包含方法的值、可写性、可枚举性等

返回值

  • 如果返回一个新的属性描述符,将替换原来的方法
  • 如果返回 voidundefined,则保持原方法不变

基本用法

typescript
// 只读方法装饰器
function readonly(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  descriptor.writable = false;
  descriptor.configurable = false;
  return descriptor;
}

class Calculator {
  @readonly
  add(a: number, b: number): number {
    return a + b;
  }
}

const calc = new Calculator();
// calc.add = function() { return 0; }; // ❌ 错误: 无法修改只读方法

修改方法行为

方法装饰器最常见的用途是修改方法的行为,例如添加日志、缓存、重试等功能:

typescript
// 缓存装饰器:缓存方法结果
function cache(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  const cacheMap = new Map<string, any>();
  
  descriptor.value = function (...args: any[]) {
    const cacheKey = JSON.stringify(args);
    
    if (cacheMap.has(cacheKey)) {
      console.log(`缓存命中: ${propertyKey}(${args.join(", ")})`);
      return cacheMap.get(cacheKey);
    }
    
    const result = originalMethod.apply(this, args);
    cacheMap.set(cacheKey, result);
    return result;
  };
  
  return descriptor;
}

class MathService {
  @cache
  expensiveCalculation(n: number): number {
    console.log(`执行计算: ${n}`);
    // 模拟耗时计算
    return n * n;
  }
}

const math = new MathService();
console.log(math.expensiveCalculation(5)); // 执行计算: 5, 输出: 25
console.log(math.expensiveCalculation(5)); // 缓存命中: expensiveCalculation(5), 输出: 25

实际应用:重试装饰器

typescript
// 自动重试装饰器
function retry(maxAttempts: number = 3, delay: number = 1000) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    
    descriptor.value = async function (...args: any[]) {
      let lastError: Error;
      
      for (let attempt = 1; attempt <= maxAttempts; attempt++) {
        try {
          return await originalMethod.apply(this, args);
        } catch (error) {
          lastError = error as Error;
          console.log(`${propertyKey} 第 ${attempt} 次尝试失败: ${error}`);
          
          if (attempt < maxAttempts) {
            await new Promise(resolve => setTimeout(resolve, delay));
          }
        }
      }
      
      throw lastError!;
    };
    
    return descriptor;
  };
}

class ApiService {
  @retry(3, 1000)
  async fetchData(url: string): Promise<any> {
    // 模拟可能失败的 API 调用
    if (Math.random() > 0.5) {
      throw new Error("网络错误");
    }
    return { data: "成功获取数据" };
  }
}

const api = new ApiService();
api.fetchData("https://api.example.com/data").then(console.log).catch(console.error);

实际应用:权限检查装饰器

typescript
// 权限检查装饰器
function requirePermission(permission: string) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    
    descriptor.value = function (...args: any[]) {
      const user = (this as any).currentUser;
      
      if (!user || !user.permissions.includes(permission)) {
        throw new Error(`需要权限: ${permission}`);
      }
      
      return originalMethod.apply(this, args);
    };
    
    return descriptor;
  };
}

class DocumentService {
  currentUser: { permissions: string[] } | null = null;
  
  @requirePermission("read")
  readDocument(id: string) {
    return `读取文档 ${id}`;
  }
  
  @requirePermission("write")
  writeDocument(id: string, content: string) {
    return `写入文档 ${id}: ${content}`;
  }
  
  @requirePermission("delete")
  deleteDocument(id: string) {
    return `删除文档 ${id}`;
  }
}

const service = new DocumentService();
service.currentUser = { permissions: ["read", "write"] };

console.log(service.readDocument("doc1")); // ✅ 成功
console.log(service.writeDocument("doc1", "content")); // ✅ 成功
// service.deleteDocument("doc1"); // ❌ 错误: 需要权限: delete

实际应用:防抖和节流装饰器

typescript
// 防抖装饰器:延迟执行,如果在延迟期间再次调用则重新计时
function debounce(delay: number) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    let timeoutId: NodeJS.Timeout;
    
    descriptor.value = function (...args: any[]) {
      clearTimeout(timeoutId);
      timeoutId = setTimeout(() => {
        originalMethod.apply(this, args);
      }, delay);
    };
    
    return descriptor;
  };
}

// 节流装饰器:限制执行频率,在指定时间内最多执行一次
function throttle(delay: number) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    let lastCallTime = 0;
    
    descriptor.value = function (...args: any[]) {
      const now = Date.now();
      if (now - lastCallTime >= delay) {
        lastCallTime = now;
        return originalMethod.apply(this, args);
      }
    };
    
    return descriptor;
  };
}

class SearchService {
  @debounce(300)
  search(query: string) {
    console.log(`搜索: ${query}`);
  }
  
  @throttle(1000)
  trackEvent(event: string) {
    console.log(`追踪事件: ${event}`);
  }
}

属性装饰器(Property Decorator)

属性装饰器应用于类的属性,可以观察或修改属性的行为。属性装饰器在属性定义时执行。

函数签名

typescript
type PropertyDecorator = (
  target: any,
  propertyKey: string | symbol
) => void;

参数说明

  • target: 类的原型对象(对于实例属性)或类构造函数(对于静态属性)
  • propertyKey: 属性名称

注意:属性装饰器不能直接修改属性的值,但可以通过 Object.defineProperty 重新定义属性。

基本用法

typescript
// 标记属性的装饰器
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;
  };
  
  Object.defineProperty(target, propertyKey, {
    get: getter,
    set: setter,
    enumerable: true,
    configurable: true
  });
}

class Person {
  @logProperty
  name: string;
  
  constructor(name: string) {
    this.name = name; // 输出: "设置 name: ..."
  }
}

const person = new Person("John");
console.log(person.name); // 输出: "获取 name: John", 然后输出: "John"

实际应用:属性验证装饰器

typescript
// 属性验证装饰器
function validate(validator: (value: any) => boolean, errorMessage?: string) {
  return function (target: any, propertyKey: string) {
    let value: any;
    
    const getter = function () {
      return value;
    };
    
    const setter = function (newVal: any) {
      if (!validator(newVal)) {
        throw new Error(errorMessage || `${propertyKey} 验证失败`);
      }
      value = newVal;
    };
    
    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true
    });
  };
}

class User {
  @validate((v: string) => v.length >= 3, "用户名至少 3 个字符")
  username: string;
  
  @validate((v: number) => v > 0 && v < 150, "年龄必须在 0-150 之间")
  age: number;
  
  @validate((v: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v), "邮箱格式不正确")
  email: string;
  
  constructor(username: string, age: number, email: string) {
    this.username = username;
    this.age = age;
    this.email = email;
  }
}

const user1 = new User("john", 30, "john@example.com"); // ✅ 成功
// const user2 = new User("jo", 30, "john@example.com"); // ❌ 错误: 用户名至少 3 个字符
// const user3 = new User("john", 200, "john@example.com"); // ❌ 错误: 年龄必须在 0-150 之间

实际应用:属性格式化装饰器

typescript
// 属性格式化装饰器
function format(formatter: (value: any) => any) {
  return function (target: any, propertyKey: string) {
    let value: any;
    
    const getter = function () {
      return value;
    };
    
    const setter = function (newVal: any) {
      value = formatter(newVal);
    };
    
    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true
    });
  };
}

class Product {
  @format((v: string) => v.trim().toUpperCase())
  name: string;
  
  @format((v: number) => Math.round(v * 100) / 100) // 保留两位小数
  price: number;
  
  constructor(name: string, price: number) {
    this.name = name;
    this.price = price;
  }
}

const product = new Product("  laptop  ", 99.999);
console.log(product.name);  // "LAPTOP"
console.log(product.price); // 100

实际应用:属性监听装饰器

typescript
// 属性变化监听装饰器
function watch(callback: (oldValue: any, newValue: any) => void) {
  return function (target: any, propertyKey: string) {
    let value: any;
    
    const getter = function () {
      return value;
    };
    
    const setter = function (newVal: any) {
      const oldValue = value;
      value = newVal;
      callback(oldValue, newVal);
    };
    
    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true
    });
  };
}

class Observable {
  @watch((oldVal, newVal) => {
    console.log(`值从 ${oldVal} 变为 ${newVal}`);
  })
  count: number = 0;
}

const obs = new Observable();
obs.count = 1; // 输出: "值从 0 变为 1"
obs.count = 2; // 输出: "值从 1 变为 2"

参数装饰器(Parameter Decorator)

参数装饰器应用于构造函数或方法的参数,主要用于依赖注入和参数验证。参数装饰器在参数定义时执行。

函数签名

typescript
type ParameterDecorator = (
  target: any,
  propertyKey: string | symbol | undefined,
  parameterIndex: number
) => void;

参数说明

  • target: 类的原型对象(对于方法参数)或类构造函数(对于构造函数参数)
  • propertyKey: 方法名称(对于方法参数)或 undefined(对于构造函数参数)
  • parameterIndex: 参数在参数列表中的索引(从 0 开始)

基本用法

typescript
// 标记参数的装饰器
function logParameter(target: any, propertyKey: string | undefined, parameterIndex: number) {
  const metadataKey = `__log_${propertyKey || "constructor"}_parameters`;
  if (!target[metadataKey]) {
    target[metadataKey] = [];
  }
  target[metadataKey].push(parameterIndex);
}

class Service {
  constructor(
    @logParameter name: string,
    @logParameter age: number
  ) {
    // ...
  }
  
  process(
    @logParameter data: string,
    @logParameter count: number
  ): void {
    // ...
  }
}

实际应用:依赖注入

typescript
// 依赖注入装饰器
const metadataKey = Symbol("injections");

function inject(token: string) {
  return function (target: any, propertyKey: string | undefined, parameterIndex: number) {
    const injections = Reflect.getMetadata(metadataKey, target) || {};
    if (!injections[propertyKey || "constructor"]) {
      injections[propertyKey || "constructor"] = [];
    }
    injections[propertyKey || "constructor"][parameterIndex] = token;
    Reflect.defineMetadata(metadataKey, injections, target);
  };
}

// 简单的依赖注入容器
class Container {
  private services = new Map<string, any>();
  
  register<T>(token: string, service: T) {
    this.services.set(token, service);
  }
  
  resolve<T>(target: new (...args: any[]) => T): T {
    const injections = Reflect.getMetadata(metadataKey, target) || {};
    const constructorInjections = injections["constructor"] || [];
    const args = constructorInjections.map((token: string) => this.services.get(token));
    return new target(...args);
  }
}

// 使用示例
class DatabaseService {
  connect() {
    return "数据库已连接";
  }
}

class LoggerService {
  log(message: string) {
    console.log(`[LOG] ${message}`);
  }
}

class UserService {
  constructor(
    @inject("DatabaseService") private db: DatabaseService,
    @inject("LoggerService") private logger: LoggerService
  ) {}
  
  getUsers() {
    this.logger.log("获取用户列表");
    return this.db.connect() + " - 用户列表";
  }
}

const container = new Container();
container.register("DatabaseService", new DatabaseService());
container.register("LoggerService", new LoggerService());

const userService = container.resolve(UserService);
console.log(userService.getUsers());
// 输出: [LOG] 获取用户列表
// 输出: 数据库已连接 - 用户列表

实际应用:参数验证

typescript
// 参数验证装饰器
function validateParam(validator: (value: any) => boolean, errorMessage?: string) {
  return function (target: any, propertyKey: string | undefined, parameterIndex: number) {
    const metadataKey = `__validate_${propertyKey || "constructor"}_parameters`;
    if (!target[metadataKey]) {
      target[metadataKey] = [];
    }
    target[metadataKey][parameterIndex] = { validator, errorMessage };
  };
}

// 方法装饰器:在方法执行前验证参数
function validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  const validators = (target as any)[`__validate_${propertyKey}_parameters`] || [];
  
  descriptor.value = function (...args: any[]) {
    for (let i = 0; i < args.length; i++) {
      const validatorInfo = validators[i];
      if (validatorInfo && !validatorInfo.validator(args[i])) {
        throw new Error(validatorInfo.errorMessage || `参数 ${i} 验证失败`);
      }
    }
    return originalMethod.apply(this, args);
  };
  
  return descriptor;
}

class Calculator {
  @validate
  divide(
    @validateParam((v: number) => typeof v === "number" && !isNaN(v), "第一个参数必须是数字")
    a: number,
    @validateParam((v: number) => v !== 0, "除数不能为零")
    b: number
  ): number {
    return a / b;
  }
}

const calc = new Calculator();
console.log(calc.divide(10, 2)); // 5
// calc.divide("10", 2); // ❌ 错误: 第一个参数必须是数字
// calc.divide(10, 0);   // ❌ 错误: 除数不能为零

装饰器组合使用

在实际开发中,经常需要组合使用多个装饰器来实现复杂的功能:

typescript
// 组合多个装饰器的示例
class ApiController {
  @requirePermission("read")
  @cache
  @log({ level: "info" })
  async getData(id: string) {
    // 方法执行顺序:
    // 1. 权限检查
    // 2. 缓存检查
    // 3. 日志记录
    // 4. 执行实际逻辑
    return { id, data: "some data" };
  }
  
  @requirePermission("write")
  @validate
  @log({ level: "warn" })
  async updateData(
    @validateParam((v: string) => v.length > 0, "ID 不能为空")
    id: string,
    @validateParam((v: any) => v !== null && v !== undefined, "数据不能为空")
    data: any
  ) {
    return { id, data, updated: true };
  }
}

注意事项

注意

  1. 执行顺序:多个装饰器的执行顺序是从下到上(对于类和方法装饰器),从最后一个参数到第一个参数(对于参数装饰器)。

  2. 类型安全:装饰器中的类型检查可能不够严格,需要开发者自行确保类型安全。

  3. 性能影响:装饰器在定义时执行,可能会影响类的初始化性能。

  4. 元数据支持:某些高级功能(如依赖注入)需要启用 emitDecoratorMetadata 并可能需要 reflect-metadata 库。

  5. 静态方法:对于静态方法,target 参数是类构造函数而不是原型对象。

最佳实践

  1. 单一职责:每个装饰器应该只负责一个功能,保持简单和可复用。

  2. 组合优于继承:使用装饰器组合来实现复杂功能,而不是创建复杂的装饰器。

  3. 文档说明:为自定义装饰器添加清晰的文档,说明参数、行为和示例。

  4. 类型定义:为装饰器提供准确的类型定义,提高类型安全性。

  5. 测试覆盖:充分测试装饰器的各种使用场景,包括边界情况。

相关链接

基于 VitePress 构建