元编程的核心思想是让程序能够审视和修改自身的结构。JavaScript 提供了多条元编程路径:反射 API、Proxy,以及我们今天要重点讨论的 Decorator 装饰器。装饰器的语法非常直观,在类或类成员前面加一个 @something,就能在定义时对它进行加工处理。这种能力看似简单,背后却隐藏着一套完整的设计哲学,它把横切关注点从业务逻辑中剥离出来,让代码结构更加清晰。

一、装饰器的本质:类定义阶段的代码拦截
装饰器并不是运行时的魔法,它发生在类被定义的那一刻。当你写下 @readonly 这样的语法时,编译器或运行时会把它转换成一次函数调用,把类的构造函数或成员描述符作为参数传进去。换句话说,装饰器本质上就是一个高阶函数,接收目标对象,返回加工后的结果。
以经典的旧版装饰器提案为例,一个方法装饰器接收三个参数:目标对象、成员名称和属性描述符。你可以在这个函数里修改描述符,比如把 writable 设为 false 来实现只读,或者替换 value 来包装原方法。这个过程完全发生在类定义阶段,而不是实例化或调用阶段,这正是它与 Proxy 最大的区别,Proxy 是运行时拦截,装饰器是定义时改写。
function readonly(target, name, descriptor) {
descriptor.writable = false;
return descriptor;
}
class Config {
@readonly
baseUrl = 'https://ipipp.com/api';
}这种定义时改写的特性带来一个好处:所有的加工操作只执行一次,运行时没有额外的拦截开销。相比之下,Proxy 每次属性访问都要经过一层代理逻辑。当然代价也很明显,装饰器一旦应用就无法撤销,灵活性不如 Proxy。理解这一层取舍,是掌握元编程工具选择的关键。
二、新旧提案的差异:从描述符操作到上下文对象
许多开发者接触装饰器是从 TypeScript 或 Babel 的旧实现开始的,但 TC39 的新版装饰器提案(目前已进入 Stage 3)在 API 设计上做了大幅调整。旧版装饰器直接操作属性描述符,而新版引入了一个上下文对象,提供了 kind、name、addInitializer 等元信息,装饰器返回的是一个函数而非描述符。
这种变化并非无缘无故。旧版提案高度依赖 Object.defineProperty 的语义,与 ES 模块、私有字段等后续特性配合时出现了不少难以调和的问题。新版提案则把装饰器定义为一组根据 kind 分发的转换规则,包括 method、field、getter、setter、class 等类型,每种类型对应不同的处理管线。这样做的好处是与语言层面特性的兼容性更好,也让装饰器的行为更可预测。
// 新版 Stage 3 装饰器示例
function logged(value, context) {
const kind = context.kind; // 'method' | 'field' 等
if (kind === 'method') {
return function (...args) {
console.log(`调用 ${context.name},参数:`, args);
return value.apply(this, args);
};
}
}
class Service {
@logged
fetchData(id) { /* ... */ }
}值得注意的是,新版提案中字段装饰器无法直接改写字段值,只能通过 addInitializer 注册初始化回调,或在字段访问时做文章。这种限制让一些旧写法需要迁移调整。如果你在维护一个使用旧版装饰器的项目,升级前务必确认编译器的支持版本和转换策略。
三、装饰器在元编程中的三大典型角色
第一个角色是行为增强。装饰器最常见的用途是在方法前后注入逻辑,比如日志记录、性能计时、错误重试。这些逻辑与业务无关,却散落在各个方法里会严重污染代码。通过装饰器集中声明,一个 @measure 就能给任意方法加上耗时统计,业务代码保持纯粹。
第二个角色是元数据注入。Angular 的依赖注入、NestJS 的路由注册,底层都依赖装饰器往类上附加元数据。以 NestJS 为例,@Controller('user') 和 @Get(':id') 会在框架启动阶段被扫描读取,框架据此构建路由表。这种声明式编程模型让模块结构一目了然,也把框架配置从命令式代码变成了可静态分析的注解。
import { Controller, Get } from '@nestjs/common';
@Controller('user')
export class UserController {
@Get(':id')
findOne(id: string) {
return this.userService.findOne(id);
}
}第三个角色是约束与校验。装饰器可以强制执行某种契约,比如 @readonly 防止属性被覆盖、@deprecated 在调用旧接口时打印警告、@requireAuth 在执行前检查权限。这类装饰器的价值在于把约定固化为代码,任何违反约定的行为都会在第一时间暴露,而不是等到运行出错才被发现。
四、装饰器、Proxy 与 defineProperty:如何选择合适的元编程工具
三者的能力有重叠,但适用场景差异明显。Object.defineProperty 是最底层的原语,适合精细控制单个属性的行为,但代码冗长,不便于复用。装饰器在语法层面包装了这套机制,用声明式的方式批量应用,但它只在定义阶段生效,无法拦截后续的动态行为。
Proxy 则是另一条路线,它能拦截整个对象的所有操作,包括 get、set、has、deleteProperty 等,甚至可以拦截不存在的属性访问。Vue 3 的响应式系统就是基于 Proxy 构建的。如果你的需求是运行时动态监控、虚拟属性、数据校验,Proxy 更合适;如果是类结构的静态增强,装饰器更简洁高效。
| 维度 | Decorator | Proxy | Object.defineProperty |
|---|---|---|---|
| 生效时机 | 类定义阶段 | 运行时 | 调用时 |
| 拦截范围 | 类或单个成员 | 整个对象 | 单个属性 |
| 运行时开销 | 无 | 每次操作都有 | 较低 |
| 可撤销性 | 不可撤销 | 可通过 revoke 撤销 | 可重新定义 |
实践中三者常常组合使用。比如用装饰器在类定义时收集元数据,再用 Proxy 包装实例做运行时校验,两条元编程路径各司其职。理解每种工具的能力边界,比记住具体 API 更重要。当装饰器提案正式落地后,它将成为 JavaScript 元编程体系中最贴近开发者日常的一环,值得提前掌握。
JavaScript Decorator元编程装饰器修改时间:2026-09-07 15:36:43