Skip to content

类装饰器

1. 装饰器的本质

在 JavaScript 中,装饰器的本质是一个函数

虽然装饰器的作用是提供元数据(Metadata),但它并非简单的静态数据声明,而是会参与到程序的运行过程中。装饰器可以修饰以下内容:

  • 类(Class)
  • 成员(属性 + 方法)
  • 参数

2. 环境准备:tsconfig 设置

由于装饰器目前在 JavaScript 中仍处于 Stage 3 阶段(尚未正式成为最终规范),在 TypeScript 中使用装饰器需要开启实验性支持。

配置说明

tsconfig.json 中,必须将 experimentalDecorators 设置为 true

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

3. 类装饰器基础

类装饰器 是紧贴在类声明之前的函数。它只有一个参数,即 类本身(构造函数)

3.1 构造函数的类型表示

在 TypeScript 中,我们通常有两种方式来描述一个构造函数:

  • Function:过于宽泛,不推荐。
  • new (...args: any[]) => any推荐做法,明确表示这是一个可以被 new 的构造函数。
typescript
// 定义一个简单的类装饰器
function classDecoration(target: new (...args: any[]) => any) {
  console.log('装饰器执行了,目标类:', target);
}

@classDecoration
class A {}

3.2 执行时机

重要特性

装饰器是在类定义时执行的,而不是在类实例化(new)时执行。

通过查看编译后的 JavaScript 代码,可以看到装饰器实际上是被 __decorate 工具函数包裹并立即执行的:

javascript
// 编译后的关键逻辑
let A = class A {};
A = __decorate([classDecoration], A); // 定义完类后立即执行装饰

4. 进阶用法

4.1 泛型约束与类型别名

为了提高代码的可读性和复用性,我们通常会定义一个 Constructor 类型别名,并使用泛型进行约束。

typescript
// 定义通用的构造函数类型别名
type Constructor<T = any> = new (...args: any[]) => T;

interface User {
  id: number;
  name: string;
  info(): void;
}

// 约束:该装饰器只能用于满足 User 接口定义的类
function validateUserClass<T extends Constructor<User>>(target: T) {
  console.log('正在校验 User 类:', target.name);
}

@validateUserClass
class Admin {
  constructor(
    public id: number,
    public name: string,
  ) {}
  info() {
    console.log('Admin info');
  }
}

4.2 装饰器工厂模式

如果我们需要向装饰器传递额外的参数,可以使用 工厂模式。装饰器工厂是一个简单的函数,它返回实际的装饰器函数。

typescript
function Logger(prefix: string) {
  // 这才是真正的装饰器函数
  return function (target: Function) {
    console.log(`[${prefix}] 装饰了类: ${target.name}`);
  };
}

@Logger('API_MODULE')
class UserService {}

4.3 通过装饰器扩展类

装饰器甚至可以返回一个新的类来替换原有的类,从而实现属性注入或方法重写。

typescript
type Constructor = new (...args: any[]) => any;

function withTimestamps<T extends Constructor>(target: T) {
  return class extends target {
    createdAt = new Date();
    printLog() {
      console.log('日志记录时间:', this.createdAt);
    }
  };
}

@withTimestamps
class Document {
  constructor(public title: string) {}
}

const doc = new Document('学习笔记');
console.log((doc as any).createdAt); // 输出当前时间

类型提示问题

虽然装饰器可以动态修改类的行为,但 TypeScript 的类型系统目前无法直接感知装饰器返回的新属性。在访问新属性时,可能需要使用 as any 或者通过接口进行类型扩展。

5. 多装饰器执行顺序

当一个类应用了多个装饰器时,它们的执行逻辑遵循以下规律:

  1. 从上到下:依次执行装饰器工厂函数(如果有)。
  2. 从下到上:依次执行真正的装饰器函数(由近及远)。
typescript
function Dec(id: number) {
  console.log(`工厂 ${id} 执行`);
  return function (target: Function) {
    console.log(`装饰器 ${id} 执行`);
  };
}

@Dec(1)
@Dec(2)
class Test {}

控制台输出:

text
工厂 1 执行
工厂 2 执行
装饰器 2 执行
装饰器 1 执行

6. 总结

  • 本质:装饰器是运行时的函数,而非静态类型。
  • 时机:类定义时即触发,非实例化时。
  • 能力:可以观察、修改甚至替换被装饰的类。
  • 顺序:工厂自上而下,装饰器自下而上。