装饰器与类增强

装饰器是即将推出的ECMAScript特性,允许我们以可重用的方式自定义类及其成员,是TypeScript 5.x的重点新特性[1]。以下从基础概念到实战应用分四步详解:

1. 装饰器基础

装饰器本质是包装函数,通过@装饰器名语法应用于类或其成员(方法、属性等),用于修改或增强目标行为。其核心特点是在类定义时执行,而非实例化时。

基础案例:@loggedMethod装饰器

typescript
复制代码
// 简单的方法装饰器:打印方法调用日志
function loggedMethod(originalMethod: any, context: ClassMethodDecoratorContext) {
  const methodName = String(context.name);
  return function(this: any, ...args: any[]) {
    console.log(`调用方法 ${methodName},参数:${args}`);
    const result = originalMethod.call(this, ...args);
    console.log(`方法 ${methodName} 返回:${result}`);
    return result;
  };
}

class UserService {
  @loggedMethod // 应用装饰器
  getUser(id: number) {
    return { id, name: "TypeScript" };
  }
}

// 类定义时装饰器已执行,实例化后调用方法触发日志
const service = new UserService();
service.getUser(1); // 输出:调用方法 getUser,参数:1;方法 getUser 返回:{id:1,name:"TypeScript"}

2. 方法装饰器:类型与参数详解

方法装饰器函数接收两个核心参数:

  • originalMethod:被装饰的原方法
  • context:上下文对象,包含name(方法名)、addInitializer(添加初始化逻辑)等属性

带类型的方法装饰器
为确保类型安全,需使用泛型定义装饰器,明确this指向、参数及返回值类型:

typescript
复制代码
function loggedMethod<This, Args extends any[], Return>(
  target: (this: This, ...args: Args) => Return,
  context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
  const methodName = context.name;
  return function(this: This, ...args: Args): Return {
    console.log(`[${methodName}] 调用参数:`, args);
    const result = target.call(this, ...args);
    console.log(`[${methodName}] 返回结果:`, result);
    return result;
  };
}

此处This约束方法的this类型,ArgsReturn分别约束参数列表和返回值类型,使装饰器具备类型推导能力。

3. 装饰器工厂:自定义装饰器行为

装饰器工厂是返回装饰器的函数,允许通过参数自定义装饰器逻辑。例如,为日志添加自定义前缀:

案例:带参数的loggedMethod工厂

typescript
复制代码
function loggedMethod(headMessage = "LOG:") {
  // 返回实际装饰器函数
  return function<This, Args extends any[], Return>(
    target: (this: This, ...args: Args) => Return,
    context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
  ) {
    const methodName = context.name;
    return function(this: This, ...args: Args): Return {
      console.log(`${headMessage}[${methodName}] 调用参数:`, args);
      const result = target.call(this, ...args);
      console.log(`${headMessage}[${methodName}] 返回结果:`, result);
      return result;
    };
  };
}

class UserService {
  // 使用工厂自定义日志前缀
  @loggedMethod("USER-LOG:")
  getUser(id: number) {
    return { id, name: "TypeScript" };
  }
}

4. 装饰器组合与注意事项

执行顺序:从下到上

多个装饰器组合时,执行顺序为从下到上(靠近方法的装饰器先执行):

typescript
复制代码
// 装饰器1:记录执行时间
function PerformanceMonitor(
  target: Function,
  context: ClassMethodDecoratorContext
) {
  return function(this: any, ...args: any[]) {
    const start = Date.now();
    const result = target.call(this, ...args);
    console.log(`执行时间:${Date.now() - start}ms`);
    return result;
  };
}

// 装饰器2:捕获错误
function ErrorCatch(
  target: Function,
  context: ClassMethodDecoratorContext
) {
  return function(this: any, ...args: any[]) {
    try {
      return target.call(this, ...args);
    } catch (error) {
      console.log(`捕获错误:${error}`);
      return null;
    }
  };
}

class DataService {
  // 装饰器顺序:先执行 PerformanceMonitor,再执行 ErrorCatch
  @ErrorCatch
  @PerformanceMonitor
  fetchData() {
    if (Math.random() > 0.5) throw new Error("网络异常");
    return "数据结果";
  }
}

调用fetchData时,实际执行顺序为:PerformanceMonitor → 原方法 → ErrorCatch(若抛出错误)。

与旧版差异

TypeScript 5.x装饰器基于ECMAScript提案,与旧版(需--experimentalDecorators)不兼容:

  • 不支持--emitDecoratorMetadata编译选项,无法自动生成元数据
  • 简化参数,移除旧版的target(类原型)、propertyKeydescriptor参数

注意事项

  1. 装饰器执行于类定义阶段,而非实例化阶段,避免在装饰器中依赖实例状态。
  2. 组合装饰器时,从下到上执行(靠近方法的先执行),与代码书写顺序相反。
  3. 禁用--emitDecoratorMetadata,如需元数据需手动实现。

实战案例:组合装饰器实现监控与容错

性能监控+错误捕获装饰器组合

typescript
复制代码
// 性能监控装饰器工厂(支持自定义日志前缀)
function PerformanceMonitor(prefix = "耗时:") {
  return function<This, Args extends any[], Return>(
    target: (this: This, ...args: Args) => Return,
    context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
  ) {
    return function(this: This, ...args: Args): Return {
      const start = Date.now();
      const result = target.call(this, ...args);
      console.log(`${prefix}${Date.now() - start}ms`);
      return result;
    };
  };
}

// 错误捕获装饰器工厂(支持自定义错误消息)
function ErrorCatch(errorMessage = "操作失败") {
  return function<This, Args extends any[], Return>(
    target: (this: This, ...args: Args) => Return,
    context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
  ) {
    return function(this: This, ...args: Args): Return | null {
      try {
        return target.call(this, ...args);
      } catch (error) {
        console.log(`${errorMessage}${error}`);
        return null;
      }
    };
  };
}

// 组合使用
class TaskService {
  @ErrorCatch("数据同步失败")
  @PerformanceMonitor("同步耗时:")
  syncData() {
    // 模拟随机失败
    if (Math.random() > 0.5) throw new Error("网络超时");
    return "同步完成";
  }
}

const task = new TaskService();
task.syncData(); // 成功:输出“同步耗时:Xms”;失败:输出“数据同步失败:网络超时”

练习:实现属性装饰器@MaxLength

需求:编写@MaxLength(limit)属性装饰器,限制类属性字符串长度,超出时抛出错误。

实现思路

  1. 装饰器工厂接收长度限制limit,返回属性装饰器。
  2. 通过context.addInitializer添加初始化逻辑,在属性赋值时校验长度。

参考代码

typescript
复制代码
function MaxLength(limit: number) {
  return function<This, Value extends string>(
    target: undefined, // 属性装饰器的target为undefined(因属性未初始化)
    context: ClassFieldDecoratorContext<This, Value>
  ) {
    const fieldName = context.name;
    // 添加初始化逻辑,拦截属性赋值
    context.addInitializer(function (this: This) {
      let value: Value;
      Object.defineProperty(this, fieldName, {
        get: () => value,
        set: (newValue: Value) => {
          if (newValue.length > limit) {
            throw new Error(`属性 ${String(fieldName)} 长度超出限制(最大${limit})`);
          }
          value = newValue;
        },
      });
    });
  };
}

// 使用示例
class User {
  @MaxLength(10)
  username: string;

  constructor(username: string) {
    this.username = username; // 初始化时触发校验
  }
}

new User("TypeScript5"); // 正常(长度9)
new User("TypeScript5.x"); // 抛出错误:属性 username 长度超出限制(最大10)

通过装饰器,我们可将类的增强逻辑(如日志、监控、校验)抽象为可重用组件,大幅提升代码复用性与可维护性。TypeScript 5.x对装饰器的支持使其成为类设计的强大工具,未来随着ECMAScript标准的稳定,其应用场景将更加广泛[1]。