装饰器是什么

装饰器(Decorator)是TypeScript中的一种特殊语法,它允许我们通过一种声明式的方式修改类、方法、属性或参数的行为。装饰器本质上是一个函数,它能够在编译时被调用,用于修改被装饰目标的特性。

从编程范式角度来看,装饰器属于元编程的范畴——即编写能够操作其他代码的代码。它借鉴了Python和Java等语言中的装饰器概念,但在TypeScript/JavaScript中有其独特的实现方式。

装饰器的基本语法

装饰器使用@expression的形式,其中expression必须是一个函数,这个函数会在运行时被调用,并接收相应的参数。

typescript 复制代码
// 定义一个简单的装饰器
function simpleDecorator(target: any) {
  console.log('装饰器被调用', target);
}

// 使用装饰器
@simpleDecorator
class MyClass {
  // 类内容
}

装饰器的类型

TypeScript支持多种类型的装饰器,每种都有不同的参数签名和行为特征。

1. 类装饰器

类装饰器应用于类构造函数,用于观察、修改或替换类定义。

typescript 复制代码
function classDecorator<T extends { new (...args: any[]): {} }>(
  constructor: T
) {
  return class extends constructor {
    newProperty = "new property";
    hello = "override";
  };
}

@classDecorator
class Greeter {
  property = "property";
  hello: string;
  constructor(m: string) {
    this.hello = m;
  }
}

2. 方法装饰器

方法装饰器用于类的方法,可以修改方法的行为或元数据。

typescript 复制代码
function methodDecorator(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;
  
  descriptor.value = function (...args: any[]) {
    console.log(`调用方法: ${propertyKey}`);
    console.log(`参数: ${JSON.stringify(args)}`);
    const result = originalMethod.apply(this, args);
    console.log(`结果: ${result}`);
    return result;
  };
}

class Calculator {
  @methodDecorator
  add(x: number, y: number): number {
    return x + y;
  }
}

3. 属性装饰器

属性装饰器用于类的属性,主要用于添加元数据或修改属性描述符。

typescript 复制代码
function propertyDecorator(target: any, propertyKey: string) {
  // 修改属性描述符
  let value: any;
  
  const getter = () => {
    console.log(`获取 ${propertyKey}: ${value}`);
    return value;
  };
  
  const setter = (newVal: any) => {
    console.log(`设置 ${propertyKey}: ${newVal}`);
    value = newVal;
  };
  
  // 替换属性
  Object.defineProperty(target, propertyKey, {
    get: getter,
    set: setter,
    enumerable: true,
    configurable: true
  });
}

class Person {
  @propertyDecorator
  name: string;
  
  constructor(name: string) {
    this.name = name;
  }
}

4. 参数装饰器

参数装饰器用于构造函数或方法的参数,主要用于依赖注入场景。

typescript 复制代码
function parameterDecorator(
  target: any,
  propertyKey: string | undefined,
  parameterIndex: number
) {
  console.log(`参数装饰器 - 目标: ${target}`);
  console.log(`方法名: ${propertyKey}`);
  console.log(`参数索引: ${parameterIndex}`);
}

class UserService {
  getUser(
    @parameterDecorator id: number,
    @parameterDecorator name: string
  ) {
    // 方法实现
  }
}

5. 访问器装饰器

访问器装饰器用于类的getter和setter方法。

typescript 复制代码
function accessorDecorator(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalGet = descriptor.get;
  const originalSet = descriptor.set;
  
  if (originalGet) {
    descriptor.get = function () {
      console.log(`获取 ${propertyKey}`);
      return originalGet.call(this);
    };
  }
  
  if (originalSet) {
    descriptor.set = function (value: any) {
      console.log(`设置 ${propertyKey}${value}`);
      return originalSet.call(this, value);
    };
  }
}

class Product {
  private _price: number = 0;
  
  @accessorDecorator
  get price(): number {
    return this._price;
  }
  
  set price(value: number) {
    this._price = value;
  }
}

装饰器执行顺序

理解装饰器的执行顺序对于正确使用它们至关重要:

typescript 复制代码
function decoratorFactory(name: string) {
  return function (target: any) {
    console.log(`${name} 装饰器执行`);
  };
}

// 执行顺序:从下到上,从右到左
@decoratorFactory('第一个')
@decoratorFactory('第二个')
@decoratorFactory('第三个')
class ExampleClass {
  // 类内容
}
// 输出:
// 第三个 装饰器执行
// 第二个 装饰器执行
// 第一个 装饰器执行

对于不同类型的装饰器,执行顺序为:

  1. 参数装饰器 → 方法装饰器 → 访问器装饰器 → 属性装饰器
  2. 每个类别内部从下到上执行
  3. 最后执行类装饰器

装饰器工厂

装饰器工厂是一个返回装饰器函数的函数,允许我们传递参数来定制装饰器的行为。

typescript 复制代码
// 装饰器工厂
function log(message: string) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value;
    
    descriptor.value = function (...args: any[]) {
      console.log(`${message}: 方法 ${propertyKey} 被调用`);
      return originalMethod.apply(this, args);
    };
  };
}

class Logger {
  @log('信息')
  logMessage(message: string) {
    console.log(message);
  }
}

元数据反射API

TypeScript的装饰器经常与反射元数据API结合使用,用于存储和检索设计时类型信息。

typescript 复制代码
import 'reflect-metadata';

function validate(
  target: any,
  propertyKey: string,
  parameterIndex: number
) {
  // 获取设计时类型信息
  const designType = Reflect.getMetadata(
    'design:type',
    target,
    propertyKey
  );
  const designParamTypes = Reflect.getMetadata(
    'design:paramtypes',
    target,
    propertyKey
  );
  const designReturnType = Reflect.getMetadata(
    'design:returntype',
    target,
    propertyKey
  );
  
  console.log('设计时类型信息:', {
    designType,
    designParamTypes,
    designReturnType
  });
}

class Validator {
  validateInput(@validate input: string) {
    // 验证逻辑
  }
}

实际应用场景

1. 依赖注入

typescript 复制代码
// 简单的依赖注入容器
class Container {
  private instances = new Map();
  
  register<T>(token: any, implementation: T) {
    this.instances.set(token, implementation);
  }
  
  resolve<T>(token: any): T {
    const instance = this.instances.get(token);
    if (!instance) {
      throw new Error(`未找到依赖: ${token}`);
    }
    return instance;
  }
}

const container = new Container();

// 注入装饰器
function inject(token: any) {
  return function (target: any, propertyKey: string, parameterIndex: number) {
    // 存储注入信息
    const existingInjections = Reflect.getMetadata('injections', target) || [];
    existingInjections[parameterIndex] = token;
    Reflect.defineMetadata('injections', existingInjections, target);
  };
}

// 可注入装饰器
function injectable(token: any) {
  return function (target: any) {
    container.register(token, new target());
  };
}

@injectable(UserService)
class UserService {
  getUsers() {
    return ['用户1', '用户2'];
  }
}

class UserController {
  constructor(
    @inject(UserService) private userService: UserService
  ) {}
  
  getUsers() {
    return this.userService.getUsers();
  }
}

2. 自动绑定

typescript 复制代码
function autoBind(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;
  return {
    configurable: true,
    enumerable: false,
    get() {
      // 将方法绑定到实例
      const boundFn = originalMethod.bind(this);
      Object.defineProperty(this, propertyKey, {
        value: boundFn,
        configurable: true,
        writable: true
      });
      return boundFn;
    }
  };
}

class EventHandler {
  @autoBind
  handleClick() {
    console.log('按钮被点击', this);
  }
}

const handler = new EventHandler();
// 即使以回调形式传递,this仍然正确指向实例
document.addEventListener('click', handler.handleClick);

3. 性能监控

typescript 复制代码
function measure(
  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}ms`);
    return result;
  };
}

class DataProcessor {
  @measure
  processLargeData(data: any[]) {
    // 模拟耗时操作
    for (let i = 0; i < 1000000; i++) {
      Math.sqrt(i);
    }
    return data.map(item => item * 2);
  }
}

装饰器组合

多个装饰器可以组合使用,创建复杂的行为:

typescript 复制代码
function readonly(target: any, propertyKey: string) {
  Object.defineProperty(target, propertyKey, {
    writable: false,
    configurable: false
  });
}

function validateRange(min: number, max: number) {
  return function (target: any, propertyKey: string) {
    let value: number;
    
    const getter = () => value;
    const setter = (newVal: number) => {
      if (newVal < min || newVal > max) {
        throw new Error(`值必须在 ${min}${max} 之间`);
      }
      value = newVal;
    };
    
    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true
    });
  };
}

class Configuration {
  @readonly
  @validateRange(0, 100)
  percentage: number = 50;
}

装饰器与JavaScript标准

需要注意的是,装饰器在TypeScript和JavaScript中的发展历程:

  1. TypeScript实现:TypeScript从早期版本就开始支持装饰器,但这是实验性功能
  2. TC39提案:装饰器提案经历了多个阶段的修改,目前处于Stage 3
  3. 差异:TypeScript的实现与最新的TC39标准存在一些差异

在TypeScript中使用装饰器时,需要在tsconfig.json中启用实验性支持:

json 复制代码
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

最佳实践和注意事项

  1. 谨慎使用:装饰器会增加代码的复杂性,只在确实需要时使用
  2. 保持简单:每个装饰器应该只负责一个明确的功能
  3. 文档化:为自定义装饰器提供清晰的文档说明
  4. 性能考虑:装饰器在编译时执行,但可能会影响运行时性能
  5. 测试:充分测试装饰器的各种使用场景

总结

装饰器是TypeScript中强大的元编程工具,它提供了一种声明式的方法来修改类和类成员的行为。通过理解不同类型的装饰器、它们的执行顺序以及实际应用场景,开发者可以创建更加模块化、可维护和可扩展的代码。

虽然装饰器目前仍然是实验性功能,但它们在Angular、NestJS等流行框架中的广泛应用证明了其价值。随着TC39提案的推进,装饰器有望成为JavaScript语言的正式特性,为元编程开辟更广阔的可能性。

掌握装饰器不仅有助于理解现代前端框架的工作原理,更能提升代码设计和架构能力,是前端开发者进阶的重要技能之一。