Skip to content

装饰器基础

概述

装饰器(Decorator)是 TypeScript 提供的一种特殊语法,用于在类、方法、属性、参数等声明上添加元数据和修改行为。装饰器本质上是一个函数,它可以在编译时或运行时对类及其成员进行增强。装饰器在 Angular、NestJS 等框架中被广泛使用,是实现依赖注入、路由装饰、验证等功能的重要工具。

注意

装饰器目前仍处于实验性阶段(Experimental),需要在 tsconfig.json 中启用 experimentalDecorators 选项。装饰器的语法和行为可能会在未来版本中发生变化。

启用装饰器

在使用装饰器之前,需要在 TypeScript 配置文件中启用装饰器支持:

json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

配置说明

  • experimentalDecorators: 启用装饰器语法支持
  • emitDecoratorMetadata: 启用装饰器元数据,用于反射和依赖注入

装饰器类型

TypeScript 支持以下类型的装饰器:

  1. 类装饰器(Class Decorator):应用于类声明
  2. 方法装饰器(Method Decorator):应用于方法
  3. 属性装饰器(Property Decorator):应用于属性
  4. 参数装饰器(Parameter Decorator):应用于参数
  5. 访问器装饰器(Accessor Decorator):应用于 getter/setter

基本语法

装饰器使用 @ 符号作为前缀,放在被装饰的声明之前:

typescript
// 装饰器函数
function myDecorator(target: any) {
  // 装饰器逻辑
}

// 使用装饰器
@myDecorator
class MyClass {
  // ...
}

装饰器函数签名

不同类型的装饰器有不同的函数签名:

typescript
// 类装饰器
function classDecorator(target: Function): void | Function {
  // target 是类的构造函数
}

// 方法装饰器
function methodDecorator(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
): void | PropertyDescriptor {
  // target: 类的原型对象
  // propertyKey: 方法名
  // descriptor: 属性描述符
}

// 属性装饰器
function propertyDecorator(target: any, propertyKey: string): void {
  // target: 类的原型对象或类构造函数
  // propertyKey: 属性名
}

// 参数装饰器
function parameterDecorator(
  target: any,
  propertyKey: string | undefined,
  parameterIndex: number
): void {
  // target: 类的原型对象或类构造函数
  // propertyKey: 方法名(如果是方法参数)或 undefined(如果是构造函数参数)
  // parameterIndex: 参数在参数列表中的索引
}

类装饰器

类装饰器应用于类声明,接收类的构造函数作为参数。

基本示例

typescript
// 定义一个简单的类装饰器
function sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

@sealed
class Greeter {
  greeting: string;
  
  constructor(message: string) {
    this.greeting = message;
  }
  
  greet() {
    return "Hello, " + this.greeting;
  }
}

修改类定义

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

typescript
// 添加静态属性的装饰器
function addStaticProperty(value: string) {
  return function <T extends { new (...args: any[]): {} }>(constructor: T) {
    return class extends constructor {
      static version = value;
    };
  };
}

@addStaticProperty("1.0.0")
class MyClass {
  name: string;
  
  constructor(name: string) {
    this.name = name;
  }
}

console.log(MyClass.version); // "1.0.0"

实际应用:日志装饰器

typescript
// 为类添加日志功能的装饰器
function logClass(target: Function) {
  const original = target;
  
  // 创建一个新的构造函数
  function constructor(...args: any[]) {
    console.log(`Instantiating ${original.name}`);
    return new original(...args);
  }
  
  // 复制原型
  constructor.prototype = original.prototype;
  
  return constructor as any;
}

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

const calc = new Calculator(); // 输出: "Instantiating Calculator"

方法装饰器

方法装饰器应用于类的方法,可以修改方法的行为。

基本示例

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

class Person {
  name: string;
  
  constructor(name: string) {
    this.name = name;
  }
  
  @readonly
  getName(): string {
    return this.name;
  }
}

const person = new Person("John");
console.log(person.getName()); // "John"

// ❌ 错误:无法修改只读方法
// person.getName = function() { return "Jane"; };

实际应用:性能监控装饰器

typescript
// 测量方法执行时间的装饰器
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} 毫秒`);
    return result;
  };
  
  return descriptor;
}

class DataProcessor {
  @measureTime
  processData(data: number[]): number[] {
    // 模拟数据处理
    return data.map(x => x * 2);
  }
}

const processor = new DataProcessor();
processor.processData([1, 2, 3, 4, 5]);
// 输出: "processData 执行时间: X 毫秒"

实际应用:参数验证装饰器

typescript
// 验证方法参数的装饰器
function validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  
  descriptor.value = function (...args: any[]) {
    // 验证第一个参数是否为数字
    if (args.length > 0 && typeof args[0] !== 'number') {
      throw new Error(`${propertyKey} 的第一个参数必须是数字`);
    }
    return originalMethod.apply(this, args);
  };
  
  return descriptor;
}

class Calculator {
  @validate
  divide(a: number, b: number): number {
    if (b === 0) {
      throw new Error('除数不能为零');
    }
    return a / b;
  }
}

const calc = new Calculator();
calc.divide(10, 2); // 5

// ❌ 错误:参数类型不匹配
// calc.divide("10", 2); // Error: divide 的第一个参数必须是数字

属性装饰器

属性装饰器应用于类的属性,可以修改属性的行为或添加元数据。

基本示例

typescript
// 标记属性为必需的装饰器
function required(target: any, propertyKey: string) {
  // 在原型上存储元数据
  if (!target._required) {
    target._required = [];
  }
  target._required.push(propertyKey);
}

class User {
  @required
  name: string;
  
  @required
  email: string;
  
  age?: number; // 可选属性
  
  constructor(name: string, email: string, age?: number) {
    this.name = name;
    this.email = email;
    this.age = age;
  }
  
  validate(): boolean {
    const required = (this.constructor as any)._required || [];
    return required.every((key: string) => this[key] !== undefined);
  }
}

const user = new User("John", "john@example.com");
console.log(user.validate()); // true

const invalidUser = new User("", "john@example.com");
console.log(invalidUser.validate()); // false(name 为空)

实际应用:格式化装饰器

typescript
// 自动格式化字符串属性的装饰器
function format(formatFn: (value: any) => any) {
  return function (target: any, propertyKey: string) {
    let value: any;
    
    const getter = function () {
      return value;
    };
    
    const setter = function (newVal: any) {
      value = formatFn(newVal);
    };
    
    // 重新定义属性
    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true
    });
  };
}

class Product {
  @format((value: string) => value.trim().toUpperCase())
  name: string;
  
  constructor(name: string) {
    this.name = name;
  }
}

const product = new Product("  laptop  ");
console.log(product.name); // "LAPTOP"(自动去除空格并转为大写)

参数装饰器

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

基本示例

typescript
// 标记参数类型的装饰器
function paramType(type: string) {
  return function (target: any, propertyKey: string | undefined, parameterIndex: number) {
    if (!target._paramTypes) {
      target._paramTypes = [];
    }
    if (!target._paramTypes[parameterIndex]) {
      target._paramTypes[parameterIndex] = [];
    }
    target._paramTypes[parameterIndex].push(type);
  };
}

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

实际应用:依赖注入标记

typescript
// 标记可注入依赖的装饰器
function inject(token: string) {
  return function (target: any, propertyKey: string | undefined, parameterIndex: number) {
    if (!target._injections) {
      target._injections = [];
    }
    target._injections[parameterIndex] = token;
  };
}

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

// 使用示例
class DatabaseService {
  connect() {
    return "Connected to database";
  }
}

class UserService {
  constructor(@inject("DatabaseService") private db: DatabaseService) {}
  
  getUsers() {
    return this.db.connect() + " - Users retrieved";
  }
}

const container = new Container();
container.register("DatabaseService", new DatabaseService());
const userService = container.resolve(UserService);
console.log(userService.getUsers()); // "Connected to database - Users retrieved"

装饰器执行顺序

当多个装饰器应用于同一个声明时,它们的执行顺序遵循特定的规则:

  1. 参数装饰器:从最后一个参数到第一个参数
  2. 方法装饰器属性装饰器访问器装饰器:从下到上
  3. 构造函数参数装饰器:从最后一个参数到第一个参数
  4. 类装饰器:从下到上
typescript
// 装饰器执行顺序示例
function first() {
  console.log("first(): 工厂函数执行");
  return function (target: any) {
    console.log("first(): 装饰器应用");
  };
}

function second() {
  console.log("second(): 工厂函数执行");
  return function (target: any) {
    console.log("second(): 装饰器应用");
  };
}

@first()
@second()
class Example {
  // ...
}

// 输出顺序:
// first(): 工厂函数执行
// second(): 工厂函数执行
// second(): 装饰器应用
// first(): 装饰器应用

装饰器工厂

装饰器工厂是一个返回装饰器函数的函数,允许传递参数来配置装饰器行为。

基本语法

typescript
// 装饰器工厂
function decoratorFactory(config: any) {
  return function (target: any) {
    // 使用 config 配置装饰器
    console.log(`装饰器配置: ${JSON.stringify(config)}`);
  };
}

@decoratorFactory({ enabled: true, level: "info" })
class MyClass {
  // ...
}

实际应用:可配置的日志装饰器

typescript
// 可配置的日志装饰器工厂
function log(config?: { level?: string; prefix?: string }) {
  const level = config?.level || "info";
  const prefix = config?.prefix || "";
  
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    
    descriptor.value = function (...args: any[]) {
      const message = `${prefix}${propertyKey}(${args.join(", ")})`;
      
      if (level === "info") {
        console.log(`[INFO] ${message}`);
      } else if (level === "warn") {
        console.warn(`[WARN] ${message}`);
      } else if (level === "error") {
        console.error(`[ERROR] ${message}`);
      }
      
      return originalMethod.apply(this, args);
    };
    
    return descriptor;
  };
}

class Calculator {
  @log({ level: "info", prefix: "计算: " })
  add(a: number, b: number): number {
    return a + b;
  }
  
  @log({ level: "warn" })
  subtract(a: number, b: number): number {
    return a - b;
  }
}

const calc = new Calculator();
calc.add(5, 3);      // 输出: [INFO] 计算: add(5, 3)
calc.subtract(10, 4); // 输出: [WARN] subtract(10, 4)

注意事项

注意

  1. 实验性特性:装饰器目前仍处于实验阶段,语法和行为可能会发生变化。在生产环境中使用需要谨慎。

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

  3. 性能影响:装饰器在编译时或运行时执行,可能会影响代码性能,特别是在大量使用装饰器的情况下。

  4. 执行时机:类装饰器在类定义时执行,方法装饰器在方法定义时执行,而不是在调用时执行。

  5. 元数据支持:某些装饰器功能(如依赖注入)需要启用 emitDecoratorMetadata 选项,并可能需要额外的库支持(如 reflect-metadata)。

最佳实践

  1. 明确用途:只在需要添加横切关注点(如日志、验证、缓存)时使用装饰器。

  2. 保持简单:装饰器逻辑应该简单明了,避免过度复杂的实现。

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

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

  5. 测试覆盖:充分测试装饰器的各种使用场景,确保行为符合预期。

相关链接

基于 VitePress 构建