Skip to content

装饰器新标准深入

一句话概述:TC39 Stage 3 装饰器 vs 旧版实验性装饰器——语法差异、迁移策略与实际应用

装饰器是什么?

定义:装饰器(Decorator)是一种特殊的函数,用于在不修改原始代码的情况下,对类、方法、属性、访问器等进行元编程增强。

两套标准

特性旧版(experimentalDecorators)新版(TC39 Stage 3)
TS 版本TS 1.5+(2015)TS 5.0+(2023)
标准状态非标准,TS 独有TC39 Stage 3,即将进入 ES 标准
参数签名(target, key, descriptor)(target, context)
依赖需要 reflect-metadata不需要额外依赖
参数装饰器✅ 支持❌ 不支持
适用场景NestJS、Angular、TypeORM新项目推荐

一、新标准装饰器语法(TS 5.0+)

方法装饰器

typescript
// 签名:(target: Function, context: ClassMethodDecoratorContext) => Function | void
function log<T extends (...args: any[]) => any>(
  target: T,
  context: ClassMethodDecoratorContext
): T {
  const methodName = String(context.name);

  return function (this: ThisParameterType<T>, ...args: Parameters<T>): ReturnType<T> {
    console.log(`[LOG] ${methodName} called with:`, args);
    const result = target.apply(this, args);
    console.log(`[LOG] ${methodName} returned:`, result);
    return result;
  } as T;
}

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

const calc = new Calculator();
calc.add(1, 2);
// [LOG] add called with: [1, 2]
// [LOG] add returned: 3

类装饰器

typescript
// 签名:(target: Function, context: ClassDecoratorContext) => Function | void
function sealed(target: Function, context: ClassDecoratorContext) {
  Object.seal(target);
  Object.seal(target.prototype);
}

function withTimestamp<T extends new (...args: any[]) => any>(
  target: T,
  context: ClassDecoratorContext
) {
  return class extends target {
    createdAt = new Date();
  };
}

@sealed
@withTimestamp
class User {
  constructor(public name: string) {}
}

const user = new User('Alice');
// (user as any).createdAt → Date 对象

属性装饰器

typescript
// 新标准中属性装饰器不能直接修改属性值
// 但可以通过 context.addInitializer 在实例化时执行逻辑
function observable(
  target: undefined,  // 属性装饰器的 target 是 undefined
  context: ClassFieldDecoratorContext
) {
  return function (this: any, initialValue: unknown) {
    console.log(`Initializing ${String(context.name)} with:`, initialValue);
    return initialValue;
  };
}

class Store {
  @observable
  count = 0;

  @observable
  name = 'default';
}

new Store();
// Initializing count with: 0
// Initializing name with: default

访问器装饰器(Accessor)

typescript
// TS 5.0 引入 accessor 关键字,配合装饰器使用
function range(min: number, max: number) {
  return function (
    target: ClassAccessorDecoratorTarget<any, number>,
    context: ClassAccessorDecoratorContext
  ): ClassAccessorDecoratorResult<any, number> {
    return {
      set(value: number) {
        if (value < min || value > max) {
          throw new RangeError(`${String(context.name)} must be between ${min} and ${max}`);
        }
        target.set.call(this, value);
      },
      get() {
        return target.get.call(this);
      },
      init(value: number) {
        if (value < min || value > max) {
          throw new RangeError(`Initial value out of range`);
        }
        return value;
      },
    };
  };
}

class Temperature {
  @range(-273.15, 1000)
  accessor celsius = 20;
}

const temp = new Temperature();
temp.celsius = 100;   // ✅
// temp.celsius = 2000; // ❌ RangeError

二、装饰器上下文(Context)

Context 对象属性

typescript
interface ClassMethodDecoratorContext {
  kind: 'method';
  name: string | symbol;
  static: boolean;       // 是否是静态方法
  private: boolean;      // 是否是私有方法
  access: {
    get(): Function;     // 获取方法引用
    has(obj: object): boolean;
  };
  addInitializer(fn: () => void): void; // 添加初始化逻辑
  metadata: DecoratorMetadata;          // TS 5.2+ 装饰器元数据
}

// 不同装饰器类型的 kind 值:
// 'class' | 'method' | 'getter' | 'setter' | 'field' | 'accessor'

addInitializer 用法

typescript
// 在类实例化时执行额外逻辑
function bound(target: Function, context: ClassMethodDecoratorContext) {
  const methodName = context.name as string;

  context.addInitializer(function (this: any) {
    // this 指向实例
    this[methodName] = this[methodName].bind(this);
  });
}

class Button {
  label = 'Click me';

  @bound
  handleClick() {
    console.log(this.label); // 始终绑定正确的 this
  }
}

const btn = new Button();
const { handleClick } = btn;
handleClick(); // "Click me" ✅ 不会丢失 this

三、装饰器工厂(带参数的装饰器)

基本模式

typescript
// 装饰器工厂 = 返回装饰器的函数
function retry(times: number) {
  return function <T extends (...args: any[]) => Promise<any>>(
    target: T,
    context: ClassMethodDecoratorContext
  ): T {
    return async function (this: any, ...args: Parameters<T>) {
      let lastError: Error | undefined;
      for (let i = 0; i < times; i++) {
        try {
          return await target.apply(this, args);
        } catch (e) {
          lastError = e as Error;
          console.warn(`Retry ${i + 1}/${times} for ${String(context.name)}`);
        }
      }
      throw lastError;
    } as T;
  };
}

class ApiClient {
  @retry(3)
  async fetchData(url: string): Promise<Response> {
    return fetch(url);
  }
}

组合多个装饰器

typescript
// 装饰器从下往上执行(靠近目标的先执行)
function first(target: any, context: ClassMethodDecoratorContext) {
  console.log('first decorator');
}

function second(target: any, context: ClassMethodDecoratorContext) {
  console.log('second decorator');
}

class Example {
  @first   // 第二个执行
  @second  // 第一个执行
  method() {}
}
// "second decorator"
// "first decorator"

四、实用装饰器示例

性能计时

typescript
function measure<T extends (...args: any[]) => any>(
  target: T,
  context: ClassMethodDecoratorContext
): T {
  return function (this: any, ...args: Parameters<T>): ReturnType<T> {
    const start = performance.now();
    const result = target.apply(this, args);
    const end = performance.now();
    console.log(`${String(context.name)}: ${(end - start).toFixed(2)}ms`);
    return result;
  } as T;
}

缓存/记忆化

typescript
function memoize<T extends (...args: any[]) => any>(
  target: T,
  context: ClassMethodDecoratorContext
): T {
  const cache = new Map<string, ReturnType<T>>();

  return function (this: any, ...args: Parameters<T>): ReturnType<T> {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      return cache.get(key)!;
    }
    const result = target.apply(this, args);
    cache.set(key, result);
    return result;
  } as T;
}

class MathService {
  @memoize
  fibonacci(n: number): number {
    if (n <= 1) return n;
    return this.fibonacci(n - 1) + this.fibonacci(n - 2);
  }
}

防抖

typescript
function debounce(delay: number) {
  return function <T extends (...args: any[]) => void>(
    target: T,
    context: ClassMethodDecoratorContext
  ): T {
    let timer: ReturnType<typeof setTimeout>;

    return function (this: any, ...args: Parameters<T>) {
      clearTimeout(timer);
      timer = setTimeout(() => target.apply(this, args), delay);
    } as T;
  };
}

class SearchInput {
  @debounce(300)
  onSearch(query: string) {
    console.log('Searching:', query);
  }
}

权限检查

typescript
function requireRole(...roles: string[]) {
  return function <T extends (...args: any[]) => any>(
    target: T,
    context: ClassMethodDecoratorContext
  ): T {
    return function (this: any, ...args: Parameters<T>): ReturnType<T> {
      const currentUser = getCurrentUser(); // 假设存在此函数
      if (!roles.includes(currentUser.role)) {
        throw new Error(`Requires role: ${roles.join(' | ')}`);
      }
      return target.apply(this, args);
    } as T;
  };
}

class AdminService {
  @requireRole('admin', 'superadmin')
  deleteUser(userId: string) {
    // 只有 admin 和 superadmin 可以执行
  }
}

五、旧版 vs 新版对比与迁移

旧版语法回顾

typescript
// tsconfig: "experimentalDecorators": true
// 旧版方法装饰器签名
function oldLog(
  target: any,                    // 类原型(实例方法)或构造函数(静态方法)
  propertyKey: string,            // 方法名
  descriptor: PropertyDescriptor  // 属性描述符
) {
  const original = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyKey}`);
    return original.apply(this, args);
  };
}

// 旧版参数装饰器(新标准不支持)
function validate(
  target: any,
  propertyKey: string,
  parameterIndex: number
) {
  // NestJS 大量使用此特性
}

迁移指南

typescript
// ========== 旧版 ==========
function oldDecorator(
  target: any,
  key: string,
  descriptor: PropertyDescriptor
) {
  const original = descriptor.value;
  descriptor.value = function (...args: any[]) {
    // 增强逻辑
    return original.apply(this, args);
  };
}

// ========== 新版 ==========
function newDecorator<T extends (...args: any[]) => any>(
  target: T,
  context: ClassMethodDecoratorContext
): T {
  return function (this: any, ...args: Parameters<T>): ReturnType<T> {
    // 增强逻辑
    return target.apply(this, args);
  } as T;
}

关键差异

方面旧版新版
target 参数类原型对象被装饰的方法本身
修改方式修改 descriptor返回替换函数
参数装饰器✅ 支持❌ 不支持
元数据reflect-metadatacontext.metadata(TS 5.2+)
共存不能与旧版共存

NestJS 等框架的情况

NestJS 依赖旧版装饰器 + 参数装饰器 + reflect-metadata,目前仍使用 experimentalDecorators。新版装饰器不支持参数装饰器,NestJS 需要等待后续 TC39 提案或自行适配。新项目如果不用 NestJS,推荐直接使用新标准装饰器


面试高频题

1. 新版装饰器和旧版有什么区别?

答案:新版(TC39 Stage 3)接收 (target, context) 两个参数,target 是被装饰的函数本身,通过返回新函数来替换。旧版接收 (target, key, descriptor),target 是类原型,通过修改 descriptor 来增强。新版不依赖 reflect-metadata,不支持参数装饰器,不能与旧版共存。

2. 装饰器的执行顺序是什么?

答案:多个装饰器应用到同一目标时,从下往上执行(靠近目标的先执行)。装饰器工厂的求值顺序是从上往下(先求值外层函数得到装饰器),然后装饰器本身从下往上执行。类中不同成员的装饰器按声明顺序执行,类装饰器最后执行。

3. 新标准为什么不支持参数装饰器?

答案:TC39 委员会认为参数装饰器的语义过于复杂,且与现有提案的设计理念不一致。参数装饰器可能作为单独的后续提案推进。这也是 NestJS 等依赖参数装饰器的框架暂时无法迁移到新标准的原因。

4. accessor 关键字是什么?

答案:TS 5.0 引入的类字段修饰符,accessor prop = value 会自动生成 getter/setter 对。配合装饰器可以拦截属性的读写操作,实现响应式、校验、转换等功能。这是新标准装饰器实现"属性拦截"的官方方式,替代了旧版通过 descriptor 修改属性的模式。