Skip to content

在 TypeScript 的高级特性中,装饰器 (Decorators) 无疑是最具“魔法”色彩的一个。它是一种特殊的声明,可以被附加到类、方法、访问器、属性或参数上,以一种声明式的语法来修改或注解它们。装饰器本质上是一种元编程(Metaprogramming)——即编写能够操作其他代码的代码。

它们在许多流行的框架(如 Angular、NestJS、TypeORM)中扮演着核心角色,用于实现依赖注入、路由映射、数据验证等功能。理解装饰器,是深入这些现代框架内部原理的钥匙。

重要警告:这是一个实验性功能

在开始之前,必须强调:装饰器目前仍然是 TypeScript 中的一项实验性功能。

这意味着:

  1. 它基于一个尚未最终确定的 TC39(ECMAScript 标准委员会)提案。

  2. 其未来的语法和行为可能会在 TypeScript 的新版本中发生变化。

  3. 要使用它,你必须在你的 tsconfig.json 文件中显式地启用 experimentalDecorators 选项:

    json
    {
      "compilerOptions": {
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true // 通常与装饰器一同使用,用于元数据反射
      }
    }

装饰器的本质:一个特殊的函数

抛开所有魔法外衣,装饰器本质上就是一个函数。这个函数会在类被定义时(而不是实例化时)被立即调用,并接收到有关其所装饰的目标的信息。TypeScript 根据装饰器被附加到的位置(类、方法等),向这个函数传入不同的参数。

让我们来探索装饰器的五种主要类型。

1. 类装饰器 (Class Decorator)

  • 作用:应用于类的构造函数。
  • 参数:接收一个参数——被装饰的类的构造函数。
  • 用途:可以用来监视、修改或替换整个类的定义。

示例:一个 sealed 装饰器,防止类被进一步扩展。

typescript
function sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
  console.log(`Class ${constructor.name} has been sealed.`);
}

@sealed
class BugReport {
  type = "report";
  title: string;

  constructor(t: string) {
    this.title = t;
  }
}

// 在类定义时,控制台就会输出: "Class BugReport has been sealed."

@sealed 语法就是将 sealed 函数应用到 BugReport 类上。

2. 方法装饰器 (Method Decorator)

  • 作用:应用于类的方法。
  • 参数:接收三个参数:
    1. target: 对于静态方法是类的构造函数,对于实例方法是类的原型。
    2. propertyKey: 方法的名称(一个字符串)。
    3. descriptor: 该方法的属性描述符 (PropertyDescriptor)。
  • 用途:可以用来监视、修改甚至替换方法的实现。这是最常用的装饰器之一。

示例:一个 log 装饰器,在方法调用前后打印日志。

typescript
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value; // 保存原始方法

  // 修改属性描述符,替换原始方法
  descriptor.value = function (...args: any[]) {
    console.log(`Calling method: ${propertyKey} with args:`, args);
    const result = originalMethod.apply(this, args);
    console.log(`Method ${propertyKey} returned:`, result);
    return result;
  };

  return descriptor;
}

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

const calc = new Calculator();
calc.add(2, 3);
// 控制台输出:
// Calling method: add with args: [2, 3]
// Method add returned: 5

3. 属性装饰器 (Property Decorator) 和 4. 访问器装饰器 (Accessor Decorator)

  • 作用:分别应用于类的属性和 get/set 访问器。
  • 参数:接收两个参数 (targetpropertyKey)。
  • 用途:主要用于记录元数据,因为它们不能直接修改属性的值(属性的值只在实例创建后才存在)。

5. 参数装饰器 (Parameter Decorator)

  • 作用:应用于构造函数或方法的参数。
  • 参数:接收三个参数:
    1. target: 类的构造函数或原型。
    2. propertyKey: 方法的名称(构造函数中为 undefined)。
    3. parameterIndex: 参数在参数列表中的索引。
  • 用途:几乎完全用于记录元数据。例如,标记哪些参数需要通过依赖注入来提供。

示例:依赖注入的元数据记录(需要 reflect-metadata 库)

typescript
import "reflect-metadata";

function Inject(service: any) {
  return function (target: Object, propertyKey: string | symbol, parameterIndex: number) {
    const existingInjections = Reflect.getOwnMetadata("injections", target, propertyKey) || [];
    existingInjections.push({ index: parameterIndex, service });
    Reflect.defineMetadata("injections", existingInjections, target, propertyKey);
  };
}

class ApiService {}

class MyComponent {
  constructor(@Inject(ApiService) private api: ApiService) {}
}

这个 @Inject 装饰器并没有改变 api 参数,它只是记录下:“第 0 个参数需要一个 ApiService 的实例”。然后,依赖注入框架会读取这些元数据并完成实际的注入工作。

装饰器工厂与组合

  • 装饰器工厂 (Decorator Factory):如果你想向装饰器传递参数,你需要创建一个“工厂函数”,它返回一个真正的装饰器函数。

    typescript
    function log(message: string) {
      // 这是工厂,返回真正的装饰器
      return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        // ... 装饰器逻辑 ...
        console.log(message);
      };
    }
    
    class C { @log("Hello from decorator!") myMethod() {} }
  • 装饰器组合:可以对一个声明应用多个装饰器。它们的执行顺序是:

    1. 评估:自上而下。
    2. 调用:自下而上(像函数组合一样)。

结论:何时使用装饰器?

装饰器是一把双刃剑。

优势:

  • 声明式:代码意图清晰,将横切关注点(如日志、权限)与核心业务逻辑分离。
  • 可组合:可以轻松地组合多个装饰器来添加复杂行为。
  • 框架利器:为框架作者提供了强大的工具来减少模板代码,实现依赖注入、ORM 等。

风险:

  • 实验性:API 可能会变。
  • 调试困难:它们增加了代码的“魔法”程度,可能让调试变得不那么直观。

建议:

  • 对于应用程序开发者:谨慎使用。在你真正需要一个清晰的横切关注点解决方案时,再考虑它。不要为了使用而使用。
  • 对于库或框架的开发者:装饰器是你的强大盟友,值得深入研究。
  • 对于所有开发者:即使你不亲自编写,也应该理解它们的工作原理,因为你很可能在你使用的框架中遇到它们。

装饰器代表了 TypeScript 与 JavaScript 语言演进的深度融合,它开启了元编程的大门,让我们能够以一种更优雅、更声明式的方式来构建复杂的软件系统。

不知道说啥了很无语了