装饰器新标准深入
一句话概述: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+)
方法装饰器
// 签名:(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类装饰器
// 签名:(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 对象属性装饰器
// 新标准中属性装饰器不能直接修改属性值
// 但可以通过 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)
// 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 对象属性
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 用法
// 在类实例化时执行额外逻辑
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三、装饰器工厂(带参数的装饰器)
基本模式
// 装饰器工厂 = 返回装饰器的函数
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);
}
}组合多个装饰器
// 装饰器从下往上执行(靠近目标的先执行)
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"四、实用装饰器示例
性能计时
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;
}缓存/记忆化
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);
}
}防抖
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);
}
}权限检查
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 新版对比与迁移
旧版语法回顾
// 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 大量使用此特性
}迁移指南
// ========== 旧版 ==========
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-metadata | context.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 修改属性的模式。