JavaScript Decorators 终于来了:Stage 3 装饰器完全指南

2764 字
14 分钟
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);
}
}
@sealed
class 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();
}
};
}
@withTimestamp
class 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);
});
}
}
@register
class UserService {}
@register
class 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); // OK
panel.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#

访问器装饰器接收一个包含 getset 的对象,返回新的 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;
}
}
// 使用
@injectable
class 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 装饰器。

最佳实践#

  1. 保持装饰器纯净:装饰器不应该产生副作用,尽量只做”增强”而不是”替换”
  2. 使用装饰器工厂传递参数@decorator(options) 比修改全局状态更清晰
  3. 利用 metadata 而不是 WeakMap:Stage 3 的 metadata API 是官方推荐的元数据方案
  4. 注意 this 绑定:方法装饰器返回的函数通过 value.call(this, ...args) 保持正确的 this
  5. 类型安全优先:在 TypeScript 中充分利用泛型约束装饰器的类型

总结#

Stage 3 装饰器是 JavaScript 语言层面的一次重大进化。它提供了统一、标准、类型安全的元编程能力,让我们可以用声明式的方式增强类和类成员的行为。从日志、缓存、权限控制到依赖注入、表单验证,装饰器几乎无处不在。

现在就开始在你的项目中尝试 Stage 3 装饰器吧!

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

JavaScript Decorators 终于来了:Stage 3 装饰器完全指南
https://boke.hackerdream.xyz/posts/javascript-decorators/
作者
晴天
发布于
2026-02-05
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
晴天
Hello, I'm 晴天.
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
125
分类
17
标签
287
总字数
257,955
运行时长
0
最后活动
0 天前

目录