25.深入装饰器 副本.md

查看飞书原文 ↗4,431 字符

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

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

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

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

这意味着:

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

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

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

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

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

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

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

1. 类装饰器 (Class Decorator)

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

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)

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

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)

5. 参数装饰器 (Parameter Decorator)

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

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 的实例”。然后,依赖注入框架会读取这些元数据并完成实际的注入工作。

装饰器工厂与组合

结论:何时使用装饰器?

装饰器是一把双刃剑。

优势:

风险:

建议:

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