JavaScript Decorators 终于来了:Stage 3 装饰器完全指南
前言
装饰器(Decorators)是 JavaScript 社区期待已久的特性。从最初的 Stage 0 提案到如今的 Stage 3,装饰器经历了多次重大改版。随着 TypeScript 5.0+ 原生支持 Stage 3 装饰器语法,以及各大打包工具的跟进,现在终于是全面拥抱装饰器的时候了。
本文将深入讲解 Stage 3 装饰器的完整语法、运行机制和实战用法,涵盖类装饰器、方法装饰器、访问器装饰器以及 metadata 元数据 API。
注意:本文讨论的是 TC39 Stage 3 提案的装饰器,与旧版实验性装饰器(TypeScript
experimentalDecorators)有本质区别。
装饰器的本质:函数即变换
装饰器的核心思想非常简单——它是一个函数,接收被装饰的目标,返回一个替换或增强后的版本。
// 最简单的装饰器:一个函数function logged(value, context) { // value: 被装饰的值(类、方法、访问器等) // context: 包含装饰器上下文信息的对象}Stage 3 装饰器统一使用 (value, context) 双参数签名,其中 context 对象包含:
interface DecoratorContext { kind: 'class' | 'method' | 'getter' | 'setter' | 'field' | 'accessor'; name: string | symbol; access?: { get?: () => unknown; set?: (value: unknown) => void }; isStatic: boolean; isPrivate: boolean; addInitializer(initializer: () => void): void; metadata: Record<string | symbol, unknown>;}kind 字段决定了当前装饰的是什么类型的成员,不同类型的装饰器在返回值上有不同的语义。
类装饰器
类装饰器是最直观的装饰器形式。它接收整个类作为参数,可以返回一个新的类来替换原始类。
基础用法
function sealed(value, context) { if (context.kind === 'class') { Object.seal(value); Object.seal(value.prototype); }}
@sealedclass User { name = 'default'; greet() { return `Hello, ${this.name}`; }}
// User 类及其原型现在被 seal 了// User.newProp = 1; // TypeError in strict mode类装饰器替换类
类装饰器可以返回一个新类来替换原始类,这在需要注入额外逻辑时非常有用:
function withTimestamp(value, context) { if (context.kind !== 'class') return;
return class extends value { createdAt = new Date();
getAge() { return Date.now() - this.createdAt.getTime(); } };}
@withTimestampclass Article { constructor(title) { this.title = title; }}
const article = new Article('Decorators Guide');console.log(article.createdAt); // 2026-04-05T...console.log(article.title); // "Decorators Guide"使用 addInitializer 注册初始化逻辑
addInitializer 允许你在类被定义后执行额外的初始化逻辑,而不需要替换整个类:
const registry = new Map();
function register(value, context) { if (context.kind === 'class') { context.addInitializer(function () { registry.set(context.name, value); }); }}
@registerclass UserService {}
@registerclass OrderService {}
console.log(registry.get('UserService')); // [class UserService]console.log(registry.get('OrderService')); // [class OrderService]方法装饰器
方法装饰器接收原始方法函数,可以返回一个新函数来替换它。这是装饰器最常用的场景之一。
日志装饰器
function log(value, context) { if (context.kind !== 'method') return;
return function (...args) { console.log(`[LOG] ${context.name}(${args.map(a => JSON.stringify(a)).join(', ')})`); const result = value.call(this, ...args); console.log(`[LOG] ${context.name} => ${JSON.stringify(result)}`); return result; };}
class Calculator { @log add(a, b) { return a + b; }
@log multiply(a, b) { return a * b; }}
const calc = new Calculator();calc.add(2, 3);// [LOG] add(2, 3)// [LOG] add => 5防抖装饰器
function debounce(delay) { return function (value, context) { if (context.kind !== 'method') return;
let timer; return function (...args) { clearTimeout(timer); timer = setTimeout(() => { value.call(this, ...args); }, delay); }; };}
class SearchBox { @debounce(300) onInput(query) { console.log('Searching for:', query); // 实际的搜索逻辑 }}注意这里使用了装饰器工厂模式——debounce(300) 返回真正的装饰器函数。这是参数化装饰器的标准写法。
缓存/记忆化装饰器
function memoize(value, context) { if (context.kind !== 'method') return;
const cache = new Map();
return function (...args) { const key = JSON.stringify(args); if (cache.has(key)) { console.log(`[CACHE HIT] ${context.name}(${key})`); return cache.get(key); } const result = value.call(this, ...args); cache.set(key, result); return result; };}
class MathUtils { @memoize fibonacci(n) { if (n <= 1) return n; return this.fibonacci(n - 1) + this.fibonacci(n - 2); }}
const math = new MathUtils();console.log(math.fibonacci(40)); // 快速返回,因为有缓存权限校验装饰器
function requireRole(role) { return function (value, context) { if (context.kind !== 'method') return;
return function (...args) { // 假设 this.currentUser 包含当前用户信息 if (!this.currentUser || !this.currentUser.roles.includes(role)) { throw new Error(`Access denied: requires role "${role}"`); } return value.call(this, ...args); }; };}
class AdminPanel { currentUser = { name: 'Alice', roles: ['admin', 'user'] };
@requireRole('admin') deleteUser(userId) { console.log(`Deleting user ${userId}`); }
@requireRole('superadmin') resetDatabase() { console.log('Resetting database...'); }}
const panel = new AdminPanel();panel.deleteUser(42); // OKpanel.resetDatabase(); // Error: Access denied: requires role "superadmin"访问器装饰器(accessor)
Stage 3 引入了全新的 accessor 关键字,它会自动为字段生成 getter/setter 对,并允许装饰器拦截读写操作。
accessor 基础
class Person { accessor name = 'anonymous';}
// 上面的代码等价于:class Person { #name = 'anonymous'; get name() { return this.#name; } set name(val) { this.#name = val; }}装饰 accessor
访问器装饰器接收一个包含 get 和 set 的对象,返回新的 get/set 来替换:
function clamped(min, max) { return function (value, context) { if (context.kind !== 'accessor') return;
const { get, set } = value;
return { get() { return get.call(this); }, set(val) { if (typeof val !== 'number') { throw new TypeError(`${context.name} must be a number`); } const clamped = Math.min(Math.max(val, min), max); set.call(this, clamped); }, init(initialValue) { // init 可以转换初始值 return Math.min(Math.max(initialValue, min), max); } }; };}
class ColorChannel { @clamped(0, 255) accessor red = 128; @clamped(0, 255) accessor green = 128; @clamped(0, 255) accessor blue = 128;}
const color = new ColorChannel();color.red = 300;console.log(color.red); // 255(被 clamp 到上限)
color.green = -50;console.log(color.green); // 0(被 clamp 到下限)响应式 accessor
function reactive(value, context) { if (context.kind !== 'accessor') return;
const { get, set } = value; const listeners = new Set();
return { get() { return get.call(this); }, set(val) { const oldVal = get.call(this); set.call(this, val); if (oldVal !== val) { listeners.forEach(fn => fn(val, oldVal, context.name)); } }, init(initialValue) { // 将 onChange 方法注入到实例上 context.addInitializer(function () { this.__listeners = this.__listeners || {}; this.__listeners[context.name] = listeners; this.onChange = this.onChange || function (prop, fn) { if (this.__listeners[prop]) { this.__listeners[prop].add(fn); } }; }); return initialValue; } };}
class FormModel { @reactive accessor username = ''; @reactive accessor email = '';}
const form = new FormModel();form.onChange('username', (newVal, oldVal) => { console.log(`username changed: "${oldVal}" -> "${newVal}"`);});
form.username = 'alice';// username changed: "" -> "alice"字段装饰器
字段装饰器不能替换字段本身,但可以通过返回一个初始化函数来变换初始值:
function uppercase(value, context) { if (context.kind !== 'field') return;
return function (initialValue) { if (typeof initialValue === 'string') { return initialValue.toUpperCase(); } return initialValue; };}
class Config { @uppercase env = 'production'; @uppercase region = 'us-east';}
const config = new Config();console.log(config.env); // "PRODUCTION"console.log(config.region); // "US-EAST"Decorator Metadata(装饰器元数据)
Stage 3 装饰器最强大的特性之一是 metadata API。每个装饰器的 context 对象都包含一个共享的 metadata 对象,允许装饰器之间传递元信息。
const VALIDATORS = Symbol('validators');
function validate(validatorFn, message) { return function (value, context) { // 在 metadata 中注册验证器 context.metadata[VALIDATORS] = context.metadata[VALIDATORS] || []; context.metadata[VALIDATORS].push({ property: context.name, validate: validatorFn, message }); };}
function required(value, context) { return validate(v => v != null && v !== '', `${context.name} is required`)(value, context);}
function minLength(len) { return function (value, context) { return validate( v => typeof v === 'string' && v.length >= len, `${context.name} must be at least ${len} characters` )(value, context); };}
class UserForm { @required @minLength(3) accessor username = '';
@required accessor email = '';}
// 通过 Symbol.metadata 获取元数据function validateInstance(instance) { const metadata = instance.constructor[Symbol.metadata]; const validators = metadata?.[VALIDATORS] || []; const errors = [];
for (const { property, validate: fn, message } of validators) { if (!fn(instance[property])) { errors.push(message); } }
return errors;}
const form = new UserForm();form.username = 'ab';form.email = '';
const errors = validateInstance(form);console.log(errors);// [// "username must be at least 3 characters",// "email is required"// ]用 Metadata 实现依赖注入
const INJECT_KEY = Symbol('inject');
function injectable(value, context) { if (context.kind !== 'class') return; // 标记这个类可以被注入 context.metadata[INJECT_KEY] = context.metadata[INJECT_KEY] || {}; context.metadata[INJECT_KEY].injectable = true;}
function inject(token) { return function (value, context) { if (context.kind !== 'field') return;
context.metadata[INJECT_KEY] = context.metadata[INJECT_KEY] || {}; context.metadata[INJECT_KEY].deps = context.metadata[INJECT_KEY].deps || []; context.metadata[INJECT_KEY].deps.push({ property: context.name, token });
return function () { // 初始值先设为 null,后续由容器注入 return null; }; };}
// 简易 DI 容器class Container { #bindings = new Map();
bind(token, factory) { this.#bindings.set(token, factory); return this; }
resolve(Class) { const instance = new Class(); const metadata = Class[Symbol.metadata]; const injectInfo = metadata?.[INJECT_KEY];
if (injectInfo?.deps) { for (const { property, token } of injectInfo.deps) { const factory = this.#bindings.get(token); if (factory) { instance[property] = factory(); } } }
return instance; }}
// 使用@injectableclass App { @inject('logger') logger; @inject('config') config;
run() { this.logger.log(`Running with env: ${this.config.env}`); }}
const container = new Container();container.bind('logger', () => ({ log: console.log }));container.bind('config', () => ({ env: 'production', port: 3000 }));
const app = container.resolve(App);app.run(); // "Running with env: production"装饰器组合与执行顺序
多个装饰器叠加时,执行顺序遵循**从下到上(从内到外)**的规则:
function first(value, context) { console.log('first evaluated'); return function (...args) { console.log('first called'); return value.call(this, ...args); };}
function second(value, context) { console.log('second evaluated'); return function (...args) { console.log('second called'); return value.call(this, ...args); };}
class Example { @first @second method() { console.log('method called'); }}
// 装饰器求值顺序: first evaluated -> second evaluated// 调用时执行顺序: first called -> second called -> method called装饰器表达式从上到下求值,但装饰器函数从下到上应用。这与函数组合 first(second(method)) 的语义一致。
TypeScript 5.0+ 实战
TypeScript 5.0 开始原生支持 Stage 3 装饰器,不需要开启 experimentalDecorators。
// tsconfig.json 不需要任何额外配置,默认支持
// 类型安全的装饰器function bound<This, Args extends unknown[], Return>( target: (this: This, ...args: Args) => Return, context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>) { const methodName = context.name; context.addInitializer(function (this: This) { (this as any)[methodName] = (this as any)[methodName].bind(this); });}
class Button { label = 'Click me';
@bound handleClick() { console.log(`Button: ${this.label}`); }}
const btn = new Button();const { handleClick } = btn;handleClick(); // "Button: Click me" — this 绑定正确完整的类型安全验证框架
type ValidatorFn = (value: unknown) => boolean;
interface ValidatorEntry { property: string | symbol; validate: ValidatorFn; message: string;}
const VALIDATORS = Symbol('validators');
function createValidator(fn: ValidatorFn, message: string) { return function <This, Value>( _value: undefined, context: ClassFieldDecoratorContext<This, Value> ) { const meta = context.metadata as Record<symbol, ValidatorEntry[]>; meta[VALIDATORS] = meta[VALIDATORS] || []; meta[VALIDATORS].push({ property: context.name, validate: fn, message }); };}
// 验证器工厂const isString = createValidator( v => typeof v === 'string', 'must be a string');
const isPositive = createValidator( v => typeof v === 'number' && v > 0, 'must be a positive number');
function maxLength(n: number) { return createValidator( v => typeof v === 'string' && v.length <= n, `must not exceed ${n} characters` );}
class Product { @isString @maxLength(100) name: string = '';
@isPositive price: number = 0;}
function validateObject<T extends object>(obj: T): string[] { const metadata = (obj.constructor as any)[Symbol.metadata] as Record<symbol, ValidatorEntry[]>; const validators = metadata?.[VALIDATORS] || []; const errors: string[] = [];
for (const { property, validate, message } of validators) { if (!validate((obj as any)[property])) { errors.push(`${String(property)}: ${message}`); } }
return errors;}与旧版装饰器的对比
| 特性 | 旧版 (experimentalDecorators) | Stage 3 |
|---|---|---|
| 参数签名 | (target, name, descriptor) | (value, context) |
| 字段装饰器 | 不标准,各实现不同 | 统一规范 |
accessor 关键字 | 无 | 原生支持 |
| Metadata | 需要 reflect-metadata | 内置 Symbol.metadata |
| 私有成员 | 无法装饰 | context.isPrivate 支持 |
| 参数装饰器 | 支持 | 暂不支持(未来可能加入) |
迁移建议:如果你的项目大量使用了旧版装饰器(如 NestJS、TypeORM),不要急于迁移。等待这些框架官方适配 Stage 3 后再统一切换。新项目建议直接使用 Stage 3 装饰器。
最佳实践
- 保持装饰器纯净:装饰器不应该产生副作用,尽量只做”增强”而不是”替换”
- 使用装饰器工厂传递参数:
@decorator(options)比修改全局状态更清晰 - 利用 metadata 而不是 WeakMap:Stage 3 的 metadata API 是官方推荐的元数据方案
- 注意 this 绑定:方法装饰器返回的函数通过
value.call(this, ...args)保持正确的this - 类型安全优先:在 TypeScript 中充分利用泛型约束装饰器的类型
总结
Stage 3 装饰器是 JavaScript 语言层面的一次重大进化。它提供了统一、标准、类型安全的元编程能力,让我们可以用声明式的方式增强类和类成员的行为。从日志、缓存、权限控制到依赖注入、表单验证,装饰器几乎无处不在。
现在就开始在你的项目中尝试 Stage 3 装饰器吧!
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!