TypeScript 装饰器是什么?新旧两套装饰器有什么区别?
一句话回答
装饰器本质上是一个函数,写成 @xxx 放在类、方法、访问器、字段前面,在类定义时被调用,拿到被装饰的目标,可以返回一个替换品来增强它。TS 里有两套互不兼容的实现:开启 experimentalDecorators 的旧版装饰器,签名是 (target, key, descriptor),支持参数装饰器,配合 emitDecoratorMetadata 能输出类型元数据,NestJS、TypeORM 等框架依赖这一套;TS 5.0 起默认支持的 ECMAScript 标准装饰器,签名是 (value, context),没有参数装饰器,也不支持 emitDecoratorMetadata。新项目不依赖这类框架时用标准装饰器。
详细解析
装饰器在什么时候执行
class 定义被求值
→ 按顺序求值各个 @ 后面的表达式(如 @log、@retry(3))
→ 调用装饰器函数,传入被装饰的方法、字段或类
→ 用返回值替换原来的成员(如果有返回值)
之后每次调用方法,执行的是替换后的函数
装饰器函数本身只在类定义时执行一次,不是每次调用方法都执行。@retry(3) 这种带参数的写法,是先调用 retry(3) 得到真正的装饰器,也叫装饰器工厂。多个装饰器叠加时,从下往上应用,离成员最近的最先包装,顺序的细节见 装饰器模式。
两套装饰器的区别
旧版(experimentalDecorators) |
标准装饰器(TS 5.0 起) | |
|---|---|---|
| 怎么启用 | tsconfig 开启 experimentalDecorators |
不开启该选项时默认使用 |
| 方法装饰器的参数 | (target, propertyKey, descriptor) |
(value, context),value 就是方法本身 |
| 修改方式 | 改写 descriptor.value 或返回新的描述符 |
返回一个新函数替换原方法 |
| 参数装饰器 | 支持,如 constructor(@Inject(TOKEN) x) |
不支持 |
| 类型元数据 | 配合 emitDecoratorMetadata 输出参数类型等 |
不支持 emitDecoratorMetadata |
| 字段装饰器 | 拿不到初始值 | 可以返回函数,加工字段的初始值 |
| 来源 | 早期的装饰器提案,TS 自己的实现 | 新版 TC39 装饰器提案 |
两套的类型检查和编译产物都不同,写给旧版的装饰器函数基本不能直接用在新版上,同一个项目只能选一套。
为什么框架还在用旧版
NestJS 的依赖注入靠的是旧版的两个能力:
- 参数装饰器:
constructor(@Inject('CONFIG') config)给构造函数参数打标记 - emitDecoratorMetadata:编译时把构造函数参数的类型写成
design:paramtypes元数据,框架运行时用reflect-metadata读出来,知道该注入哪个类
标准装饰器暂时没有这两样能力,所以这类框架的项目要开启 experimentalDecorators 和 emitDecoratorMetadata。NestJS 的依赖注入怎么用见 NestJS 的依赖注入。
运行时支持
装饰器会被编译成真实的运行时代码(上面的替换过程),实际项目里一般由 TS 或 Babel 编译后再运行,不要假定目标环境能直接执行 @ 语法。Node.js 内置的类型剥离只删除类型,它的文档明确说明遇到装饰器会报语法错误。
代码示例:日志装饰器的两种写法
标准装饰器:
function log<This, Args extends unknown[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>,
) {
const name = String(context.name)
// 返回的新函数会替换原方法
return function (this: This, ...args: Args): Return {
console.log(`调用 ${name}`, args)
return target.call(this, ...args)
}
}
class OrderService {
@log
create(id: number) {
return { id }
}
}
new OrderService().create(1) // 输出:调用 create [ 1 ]
旧版装饰器(tsconfig 需开启 experimentalDecorators):
function log(target: object, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value
// 改写描述符的 value,替换原方法
descriptor.value = function (this: unknown, ...args: unknown[]) {
console.log(`调用 ${propertyKey}`, args)
return original.apply(this, args)
}
}
class OrderService {
@log
create(id: number) {
return { id }
}
}
new OrderService().create(1) // 输出同上
面试官可能追问
标准装饰器的 context 里有什么?
包括 kind('method'、'field'、'class' 等)、name、static、private、access(读写该成员的方法),以及 addInitializer。addInitializer 注册的函数会在构造实例时执行(静态成员则在类初始化时),常用来做自动绑定 this:在里面执行 this[context.name] = this[context.name].bind(this)。TS 5.2 起还有 context.metadata,用来在装饰器之间共享元数据。
旧版的 emitDecoratorMetadata 到底输出了什么?
对被装饰的成员,编译器额外生成 design:type、design:paramtypes、design:returntype 这几项元数据,值是运行时的构造函数,比如 [UserRepository, ConfigService]。只有 class 这类在运行时存在的值能被记录,interface 编译后不存在,会退化成 Object,所以 NestJS 里注入接口要用字符串或 symbol 作为 token。
装饰器和高阶函数有什么区别?
思想一样,都是在不改原函数的前提下包一层。高阶函数作用于独立的函数,随时可以调用;装饰器是语法,只能用在类和类成员上,在类定义时自动应用。函数式的写法见 装饰器模式。
易错点
- 装饰器函数在类定义时执行,不是每次调用方法时执行;每次调用执行的是它返回的新函数
- 开不开
experimentalDecorators决定了整个项目用哪一套,两套的装饰器函数不能混用 - 标准装饰器没有参数装饰器,依赖参数注入的框架不能直接切换
- 装饰器不是纯类型语法,会生成运行时代码,只做类型剥离的工具不支持
AI 模拟面试官
用自己的话回答,AI 对照参考答案打分、指出遗漏,再追问,最多 3 轮
这道题你掌握了吗?
选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。
学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。