Legacy 装饰器(下)
本教程共 80 篇 · 第 69 篇 · 更新于 2026-08-10 · 约 13 分钟阅读
本节目标:掌握剩下的两种 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;
}
}
装饰器修改了 x 的 configurable 属性,之后就无法再更改 get x 的描述符配置。
一个重要的限制
不能同时装饰同一个属性的 getter 和 setter:
class Person {
#name: string = "";
@logAccessor // ✅ 可以装饰 setter
set name(n: string) {
this.#name = n;
}
// @logAccessor // ❌ 再加一个就报错
get name() {
return this.#name;
}
}
原因在于:装饰器收到的 descriptor 里同时包含了 get 和 set(如果有的话)。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.defineMetadata 和 Reflect.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:paramtypes、design:type、design:returntype这三个 key 是由 TypeScript 编译器自动生成的。“design” 不是随便拼的,它来自emitDecoratorMetadata的编译行为。
元数据的存储结构
Reflect.defineMetadata 的第三个参数 target 决定了元数据存在哪个对象上:
- 传原型对象
target.prototype,元数据绑定到实例。 - 传类本身,元数据绑定到静态层。
读取时也要指定相同的对象,否则拿不到。这和 WeakMap 的机制类似——元数据不会阻止对象被垃圾回收。
小结
legacy 装饰器五种类型全部讲完了:
- 类装饰器:拿构造函数,可以替换类。
- 方法装饰器:拿
descriptor,可以包装/替换方法。 - 属性装饰器:拿属性和原型,能力受限,只能通过
Object.defineProperty绕道。 - 存取器装饰器:跟方法装饰器签名一致,但不能同时装饰 getter 和 setter。
- 参数装饰器:只能标记位置,必须配合
reflect-metadata才有实际价值。
legacy 装饰器的设计不算完美——属性装饰器功能受限、参数装饰器不能拦截、多装饰器顺序容易踩坑。这些不足正是 TC39 设计标准装饰器时重点改进的地方。下一章就来看标准装饰器怎么做。