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

TypeScript 入门教程

Legacy 装饰器(下)

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

TypeScriptTypeScript 入门教程装饰器Legacy Decorator参数装饰器存取器装饰器reflect-metadata元数据

本节目标:掌握剩下的两种 legacy 装饰器——参数装饰器和存取器装饰器。理解多个装饰器摞在一起时的执行顺序规则,以及 reflect-metadata 如何给装饰器加上元数据能力。

参数装饰器:标记参数位置

参数装饰器只能用来”标记”,不能修改方法行为。它的签名三参数:

function logParam(
  target: any,           // 原型(实例方法)或构造函数(静态方法)
  propertyKey: string,   // 方法名
  parameterIndex: number // 参数位置,从 0 开始
) {
  console.log(`${propertyKey} 的第 ${parameterIndex} 个参数被标记`);
}

class Demo {
  greet(@logParam name: string, @logParam age: number) {
    console.log(`${name}, ${age}`);
  }
}
// greet 的第 1 个参数被标记
// greet 的第 0 个参数被标记

注意输出顺序:第 1 个参数先输出,第 0 个后输出。这是 legacy 装饰器”逆序执行”的体现,后面会细讲。

参数装饰器本身不能修改或拦截参数。它的价值和 reflect-metadata 绑定在一起——先把”这个位置的参数是什么类型”记录下来,给框架后续使用。

Note

参数装饰器只在 legacy 模式中有。TC39 Stage 3 标准装饰器移除了参数装饰器,这成为 NestJS 和 Angular 迁移的最大障碍。

存取器装饰器:管控 getter/setter

存取器装饰器的签名跟方法装饰器一模一样:

function configurable(value: boolean) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    descriptor.configurable = value;
  };
}

class Point {
  private _x: number = 0;

  @configurable(false)
  get x() {
    return this._x;
  }
}

装饰器修改了 xconfigurable 属性,之后就无法再更改 get x 的描述符配置。

一个重要的限制

不能同时装饰同一个属性的 getter 和 setter

class Person {
  #name: string = "";

  @logAccessor  // ✅ 可以装饰 setter
  set name(n: string) {
    this.#name = n;
  }

  // @logAccessor  // ❌ 再加一个就报错
  get name() {
    return this.#name;
  }
}

原因在于:装饰器收到的 descriptor 里同时包含了 getset(如果有的话)。TypeScript 的规则是只装饰先出现的那个存取器,装饰一次就够了。

多装饰器的执行顺序

这是 legacy 装饰器最让人困惑的地方。先把结论说清楚:

原则一:工厂函数(外层)从上到下执行,装饰器函数(内层)从下到上执行。

function first() {
  console.log("first(): 工厂执行");
  return function (...args: any[]) {
    console.log("first(): 装饰器执行");
  };
}

function second() {
  console.log("second(): 工厂执行");
  return function (...args: any[]) {
    console.log("second(): 装饰器执行");
  };
}

class Example {
  @first()
  @second()
  method() {}
}

// 输出:
// first(): 工厂执行
// second(): 工厂执行
// second(): 装饰器执行   ← 后写的先执行!
// first(): 装饰器执行

这符合数学上的函数复合:@first @second f 等价于 first(second(f))

原则二:不同类型装饰器之间,由内向外执行。

执行顺序装饰器类型
1(最先)实例成员的参数装饰器 → 方法/属性/存取器装饰器
2静态成员的参数装饰器 → 方法/属性/存取器装饰器
3构造函数的参数装饰器
4(最后)类装饰器

写个综合示例验证:

function d(key: string): any {
  return function () {
    console.log("执行:", key);
  };
}

@d("类装饰器")
class C {
  @d("静态方法")
  static sm() {}

  @d("实例方法")
  im() {}

  constructor(@d("构造参数") foo: any) {}
}

// 输出:
// 执行: 实例方法
// 执行: 静态方法
// 执行: 构造参数
// 执行: 类装饰器

原则三:同一方法里的参数装饰器,总早于该方法装饰器。

class C {
  @d("方法装饰器")
  m(@d("参数1") a: any, @d("参数2") b: any) {}
}

// 输出:
// 执行: 参数2     ← 逆序:后面的参数先执行
// 执行: 参数1
// 执行: 方法装饰器  ← 参数都执行完后才轮到方法

三个原则总结成人话:工厂从上往下算,装饰器从下往上跑,参数比方法跑得早,实例比静态跑得早,最后才轮到类和构造函数。

reflect-metadata:把类型信息存起来

reflect-metadata 是一个独立的 npm 包,和 TypeScript 的 emitDecoratorMetadata 编译选项配合使用。它的作用是让装饰器能在运行时读取编译时产生的类型信息。

先安装:

npm install reflect-metadata

在入口文件最顶部引入一次:

import "reflect-metadata";

然后开启编译选项:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

接下来,用 Reflect.defineMetadataReflect.getMetadata 来存取元数据:

import "reflect-metadata";

// 存入元数据
function Role(role: string) {
  return function (target: any) {
    Reflect.defineMetadata("role", role, target);
  };
}

@Role("admin")
class AdminController {}

// 读取元数据
const role = Reflect.getMetadata("role", AdminController);
console.log(role); // "admin"

更常见的用法是利用 emitDecoratorMetadata 自动记录参数类型:

class UserService {
  constructor(
    private db: Database,
    private logger: Logger
  ) {}
}

// 编译后 TS 自动插入了元数据,可以在运行时读取参数类型
const paramTypes = Reflect.getMetadata("design:paramtypes", UserService);
console.log(paramTypes); // [Database, Logger]

这就是依赖注入框架(如 NestJS、Angular)的核心原理——它们通过读取 design:paramtypes 元数据,知道构造函数需要什么依赖,然后自动注入。

Note

design:paramtypesdesign:typedesign:returntype 这三个 key 是由 TypeScript 编译器自动生成的。“design” 不是随便拼的,它来自 emitDecoratorMetadata 的编译行为。

元数据的存储结构

Reflect.defineMetadata 的第三个参数 target 决定了元数据存在哪个对象上:

  • 传原型对象 target.prototype,元数据绑定到实例。
  • 传类本身,元数据绑定到静态层。

读取时也要指定相同的对象,否则拿不到。这和 WeakMap 的机制类似——元数据不会阻止对象被垃圾回收。

小结

legacy 装饰器五种类型全部讲完了:

  • 类装饰器:拿构造函数,可以替换类。
  • 方法装饰器:拿 descriptor,可以包装/替换方法。
  • 属性装饰器:拿属性和原型,能力受限,只能通过 Object.defineProperty 绕道。
  • 存取器装饰器:跟方法装饰器签名一致,但不能同时装饰 getter 和 setter。
  • 参数装饰器:只能标记位置,必须配合 reflect-metadata 才有实际价值。

legacy 装饰器的设计不算完美——属性装饰器功能受限、参数装饰器不能拦截、多装饰器顺序容易踩坑。这些不足正是 TC39 设计标准装饰器时重点改进的地方。下一章就来看标准装饰器怎么做。