首页 / TypeScript 入门教程 / Legacy 装饰器(上)

TypeScript 入门教程

Legacy 装饰器(上)

本教程共 80 篇 · 第 68 篇 · 更新于 2026-08-10 · 约 14 分钟阅读

TypeScriptTypeScript 入门教程装饰器Legacy Decorator类装饰器方法装饰器属性装饰器

本节目标:掌握 legacy 装饰器的三种核心类型——类装饰器、方法装饰器、属性装饰器。理解它们的参数签名、返回值含义,以及装饰器工厂的用法。

先确认你的 tsconfig.json 开了这个选项:

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

legacy 装饰器本质上就是函数@ 后面跟一个函数名(或者函数调用表达式),编译器在类定义时自动调用这个函数,传入对应的参数。

类装饰器:拿构造函数开刀

类装饰器只有一个参数——类的构造函数

function reportable(constructor: Function) {
  console.log(`类 ${constructor.name} 被装饰了`);
}

@reportable
class UserService {
  getUsers() {
    return ["Alice", "Bob"];
  }
}
// 输出:类 UserService 被装饰了

不需要 new,类定义时就会触发装饰器。这个时机很重要——装饰器只执行一次,发生在代码加载阶段。

返回值:可以替换整个类

类装饰器的返回值如果是函数,就会替换原始构造函数

function addVersion<T extends new (...args: any[]) => {}>(constructor: T) {
  return class extends constructor {
    version = "1.0.0";
  };
}

@addVersion
class ApiClient {
  fetchData() {
    console.log("fetching...");
  }
}

const client = new ApiClient();
console.log((client as any).version); // "1.0.0"

装饰器返回了一个继承原始类的新类,新类多了 version 属性。注意这里用 extends constructor 保持了原型链,所以 instanceof 仍然正常。

如果不返回任何值(void),装饰器就只能做”副作用”——比如打日志、用 Object.seal 冻结类等,但不能改变类的结构。

装饰器工厂:给装饰器传参数

有时候你希望装饰器是可配置的。@sealed 不错,但如果你想要 @version("2.0") 呢?

这时候需要装饰器工厂——一个返回装饰器函数的普通函数:

function version(v: string) {
  // 外层函数接收配置参数
  return function <T extends new (...args: any[]) => {}>(constructor: T) {
    // 内层函数才是真正的装饰器
    return class extends constructor {
      version = v;
    };
  };
}

@version("3.0.0")  // 先调用 version("3.0.0"),得到装饰器函数,再应用
class App {}

@version("3.0.0") 的执行过程:

  1. 先执行 version("3.0.0"),返回一个装饰器函数。
  2. 再把这个装饰器函数应用到 class App

你会在几乎所有实际项目里看到装饰器工厂。Angular 的 @Component({ ... })、NestJS 的 @Get("/users")——本质上都是装饰器工厂。

Warning

以上 MinLength 示例仅为演示属性装饰器的能力。它将值通过闭包存在原型上,多个实例共享同一个闭包变量,实际项目中使用会导致状态互相覆盖的 Bug。生产环境中请使用独立的校验层或 Reflect.defineMetadata 方案。

方法装饰器:拦截和增强方法

方法装饰器的签名是三个参数:

function log(
  target: any,           // 类的原型(实例方法)或构造函数(静态方法)
  propertyKey: string,   // 方法名
  descriptor: PropertyDescriptor  // 方法的属性描述对象
) {
  // ...
}

最核心的是第三个参数 descriptor——它包含 value(方法本身)、writableenumerableconfigurable。通过修改 descriptor.value,你可以包装原始方法:

function measureTime(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const original = descriptor.value;

  descriptor.value = function (...args: any[]) {
    const start = performance.now();
    const result = original.apply(this, args);
    const end = performance.now();
    console.log(`${propertyKey} 执行耗时:${(end - start).toFixed(2)}ms`);
    return result;
  };
}

class Calculator {
  @measureTime
  heavyComputation(n: number): number {
    // 模拟耗时操作
    for (let i = 0; i < 1e7; i++) {}
    return n * 2;
  }
}

const calc = new Calculator();
calc.heavyComputation(42);
// 输出:heavyComputation 执行耗时:X.XXms

装饰器在原始方法外面包了一层计时逻辑。关键细节:

  • original.apply(this, args) 保证了 this 指向正确。
  • args 原封不动地透传给原始方法。

返回值:替换整个方法

方法装饰器也可以返回一个新的描述对象,或直接返回一个新函数替换原来方法:

function deprecate(message: string) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const original = descriptor.value;
    descriptor.value = function (...args: any[]) {
      console.warn(`${propertyKey} 已废弃:${message}`);
      return original.apply(this, args);
    };
    return descriptor;
  };
}

这是装饰器工厂 + 方法包装的组合用法,在实际项目中非常常见。

属性装饰器:有心无力

属性装饰器的签名最短——只有两个参数:

function logProp(target: any, propertyKey: string) {
  console.log(`属性 ${propertyKey} 被装饰`);
}

class User {
  @logProp
  name: string = "Alice";
}
// 输出:属性 name 被装饰

两个注意点:

  • target:实例属性的话,是类的原型(不是实例本身)。
  • 没有第三个参数 descriptor:你拿不到属性的值,也没有描述对象可以改。

这意味着属性装饰器的能力非常有限。你无法在装饰器里直接读写属性的值。那如果想对属性加校验逻辑怎么办?绕道——用 Object.defineProperty 在原型上重新定义存取器:

function MinLength(limit: number) {
  return function (target: any, propertyKey: string) {
    let value: string;

    Object.defineProperty(target, propertyKey, {
      get: () => value,
      set: (newVal: string) => {
        if (newVal.length < limit) {
          throw new Error(`${propertyKey} 长度不能小于 ${limit}`);
        }
        value = newVal;
      },
      enumerable: true,
      configurable: true,
    });
  };
}

class User {
  @MinLength(6)
  password: string;

  constructor(password: string) {
    this.password = password;
  }
}

// new User("12345"); // ❌ Error:password 长度不能小于 6
const u = new User("securePass"); // ✅

这算是属性装饰器在 legacy 模式下最实用的用例了——拦截属性的赋值操作。注意,返回值的装饰器函数会被忽略,legacy 属性装饰器的返回值不起任何作用。

Tip

legacy 属性装饰器之所以不给 descriptor,是因为装饰器执行时实例还不存在,属性的值还没绑定。想对属性做更多操作,要么用 Object.defineProperty 的 hack,要么考虑标准装饰器——标准装饰器在这方面设计得更合理。

小结

装饰器类型参数返回值作用
类装饰器(constructor)返回新类 → 替换原类
方法装饰器(target, key, descriptor)返回新描述对象 → 覆盖原方法
属性装饰器(target, key)返回值被忽略

装饰器工厂不是新的装饰器类型,只是一种模式——用外层函数封装参数,返回真正的装饰器函数。下一章继续讲 legacy 装饰器的另外两种类型(参数装饰器、存取器装饰器)和执行顺序规则。