装饰器高级应用

装饰器是 TypeScript 中实现元编程的核心机制,通过它可以在不修改类或类成员原始定义的情况下,添加元数据、扩展功能或修改行为。本节将深入探讨装饰器的高级应用场景,包括类装饰器、属性装饰器的实现方式,装饰器的执行顺序,新旧版本装饰器的差异对比,以及通过实战案例与练习巩固知识点。

一、类装饰器:类型注册与元数据注入

类装饰器是应用于类声明的装饰器,其核心作用是接收类的构造函数作为参数,从而实现类的注册、元数据添加或行为修改。典型应用场景包括服务注册、ORM 模型标记、依赖注入等。

基础示例:实现一个简单的类注册装饰器,在类定义时自动输出注册信息:

typescript
复制代码
// 类装饰器:接收构造函数作为参数
function register(constructor: Function) {
  console.log(`Registered class: ${constructor.name}`);
  // 可通过构造函数原型添加元数据
  constructor.prototype.isRegistered = true;
}

@register // 应用装饰器
class User {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}
// 输出:Registered class: User
console.log(User.prototype.isRegistered); // 输出:true

类装饰器的核心价值在于元数据管理,通过装饰器可以为类附加额外信息(如数据库表名、服务标识等),这些信息可在运行时通过反射机制读取,为框架或工具提供配置依据。

二、属性装饰器:字段行为与元数据定义

属性装饰器应用于类的属性声明,用于监控属性定义、添加字段元数据或修改属性行为。其接收两个参数:target(类的原型对象)和 propertyKey(属性名),在属性定义阶段执行。

基础示例:实现一个属性日志装饰器,在属性定义时输出字段信息:

typescript
复制代码
// 属性装饰器:接收原型对象和属性名
function logProperty(target: any, propertyKey: string) {
  console.log(`Property declared: ${propertyKey} (on ${target.constructor.name})`);
  // 为属性添加元数据(可通过 Reflect API 存储)
  Reflect.defineMetadata('logProperty', true, target, propertyKey);
}

class User {
  @logProperty // 应用属性装饰器
  name: string;
  
  @logProperty
  age: number;
}
// 输出:
// Property declared: name (on User)
// Property declared: age (on User)

属性装饰器常与类装饰器配合使用,例如在 ORM 框架中,类装饰器标记表名,属性装饰器标记字段类型和约束。

三、装饰器执行顺序:从下到上的链式调用

当多个装饰器应用于同一目标(类、方法或属性)时,其执行顺序遵循从下到上(或从右到左)的规则,即靠近目标的装饰器先执行。这一机制类似于函数组合,允许装饰器按特定顺序链式处理目标。

类装饰器顺序示例

typescript
复制代码
function decorator1(constructor: Function) {
  console.log('decorator1 executed');
}

function decorator2(constructor: Function) {
  console.log('decorator2 executed');
}

@decorator1 // 上层装饰器
@decorator2 // 下层装饰器(靠近类)
class A {}
// 输出顺序:decorator2 executed → decorator1 executed

方法装饰器顺序示例

typescript
复制代码
function bound(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log('bound executed');
  return descriptor;
}

function loggedMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log('loggedMethod executed');
  return descriptor;
}

class Greeter {
  @bound      // 下层装饰器(靠近方法)
  @loggedMethod // 上层装饰器
  greet() {}
}
// 输出顺序:bound executed → loggedMethod executed

关键规则:装饰器执行顺序为「从下到上」,即后声明的装饰器(靠近目标)先执行。在实际开发中,需注意装饰器依赖关系,例如依赖注入装饰器应优先于日志装饰器执行。

四、新旧装饰器差异:从实验特性到标准语法

TypeScript 装饰器经历了从实验性特性(experimentalDecorators)到 TC39 标准语法的演进,两者在功能支持和语法规范上存在显著差异:

特性 旧版(experimentalDecorators) 新版(标准装饰器)
语法规范 非标准实验性语法 符合 TC39 装饰器提案(Stage 3)
参数装饰器 支持(parameterDecorator 暂不支持(需通过其他方式实现参数处理)
元数据发射 支持 emitDecoratorMetadata 选项 不支持自动发射元数据(需手动实现)
装饰器返回值 类装饰器可返回新构造函数替换原类 所有装饰器返回值均被忽略
执行时机 类定义阶段执行 类初始化阶段执行(更接近运行时)
兼容性 需显式开启 experimentalDecorators 选项 TypeScript 5.0+ 默认支持(需配置 target: ES2022+

迁移注意事项:若从旧版装饰器迁移至新版,需移除参数装饰器依赖,元数据管理需通过 Reflect.metadata 手动实现,且装饰器不再支持替换类构造函数。

五、实战案例:实现简易依赖注入容器

依赖注入(DI)是装饰器的经典应用场景,通过类装饰器注册服务,属性装饰器自动注入依赖,实现组件解耦。以下是一个简化的 DI 容器实现:

typescript
复制代码
// 1. 定义服务注册表
const serviceRegistry = new Map<string, any>();

// 2. 类装饰器:注册服务
function Injectable(identifier: string) {
  return (constructor: Function) => {
    serviceRegistry.set(identifier, constructor);
  };
}

// 3. 属性装饰器:注入依赖
function Inject(identifier: string) {
  return (target: any, propertyKey: string) => {
    // 在类实例化时注入依赖
    Object.defineProperty(target, propertyKey, {
      get() {
        const ServiceClass = serviceRegistry.get(identifier);
        return new ServiceClass();
      }
    });
  };
}

// 4. 使用示例
@Injectable('logger') // 注册日志服务
class LoggerService {
  log(message: string) {
    console.log(`[Log]: ${message}`);
  }
}

class UserService {
  @Inject('logger') // 注入日志服务
  private logger!: LoggerService;

  createUser() {
    this.logger.log('User created'); // 调用注入的服务
  }
}

// 测试
const userService = new UserService();
userService.createUser(); // 输出:[Log]: User created

六、练习:实现 ORM 模型元数据装饰器

目标:编写类装饰器 @Table 和属性装饰器 @Column,为 ORM 模型类添加表名和字段元数据,以便后续生成数据库表结构。

步骤

  1. 实现 @Table(tableName: string):为类添加表名元数据,存储在类的 tableMetadata 属性中。
  2. 实现 @Column(type: string):为属性添加字段类型元数据,存储在类的 columnMetadata 对象中(键为属性名,值为字段类型)。

参考实现

typescript
复制代码
// 类装饰器:添加表名元数据
function Table(tableName: string) {
  return (constructor: Function) => {
    constructor.prototype.tableMetadata = { tableName };
  };
}

// 属性装饰器:添加字段元数据
function Column(type: string) {
  return (target: any, propertyKey: string) => {
    if (!target.columnMetadata) {
      target.columnMetadata = {};
    }
    target.columnMetadata[propertyKey] = { type };
  };
}

// 使用示例
@Table('users')
class User {
  @Column('int')
  id: number;

  @Column('varchar(50)')
  name: string;

  @Column('datetime')
  createdAt: Date;
}

// 验证元数据
const user = new User();
console.log(user.tableMetadata); // { tableName: 'users' }
console.log(user.columnMetadata); 
// { id: { type: 'int' }, name: { type: 'varchar(50)' }, createdAt: { type: 'datetime' } }

通过上述练习,可以深入理解装饰器在元数据管理中的核心作用,为后续学习 TypeORM、NestJS 等框架的装饰器应用打下基础。

装饰器作为 TypeScript 元编程的核心工具,其灵活的元数据管理能力使其在框架开发、依赖注入、ORM 映射等场景中发挥重要作用。掌握装饰器的高级应用,能够显著提升代码的可维护性和扩展性。在实际开发中,需注意新旧装饰器的差异,优先采用符合标准的新版装饰器语法。