Legacy 装饰器(上)
本教程共 80 篇 · 第 68 篇 · 更新于 2026-08-10 · 约 14 分钟阅读
本节目标:掌握 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") 的执行过程:
- 先执行
version("3.0.0"),返回一个装饰器函数。 - 再把这个装饰器函数应用到
class App。
你会在几乎所有实际项目里看到装饰器工厂。Angular 的 @Component({ ... })、NestJS 的 @Get("/users")——本质上都是装饰器工厂。
Warning以上 MinLength 示例仅为演示属性装饰器的能力。它将值通过闭包存在原型上,多个实例共享同一个闭包变量,实际项目中使用会导致状态互相覆盖的 Bug。生产环境中请使用独立的校验层或
Reflect.defineMetadata方案。
方法装饰器:拦截和增强方法
方法装饰器的签名是三个参数:
function log(
target: any, // 类的原型(实例方法)或构造函数(静态方法)
propertyKey: string, // 方法名
descriptor: PropertyDescriptor // 方法的属性描述对象
) {
// ...
}
最核心的是第三个参数 descriptor——它包含 value(方法本身)、writable、enumerable、configurable。通过修改 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 属性装饰器的返回值不起任何作用。
Tiplegacy 属性装饰器之所以不给
descriptor,是因为装饰器执行时实例还不存在,属性的值还没绑定。想对属性做更多操作,要么用Object.defineProperty的 hack,要么考虑标准装饰器——标准装饰器在这方面设计得更合理。
小结
| 装饰器类型 | 参数 | 返回值作用 |
|---|---|---|
| 类装饰器 | (constructor) | 返回新类 → 替换原类 |
| 方法装饰器 | (target, key, descriptor) | 返回新描述对象 → 覆盖原方法 |
| 属性装饰器 | (target, key) | 返回值被忽略 |
装饰器工厂不是新的装饰器类型,只是一种模式——用外层函数封装参数,返回真正的装饰器函数。下一章继续讲 legacy 装饰器的另外两种类型(参数装饰器、存取器装饰器)和执行顺序规则。