TC39 Stage 3 标准装饰器
本教程共 80 篇 · 第 70 篇 · 更新于 2026-08-10 · 约 15 分钟阅读
本节目标:掌握 TC39 Stage 3 标准装饰器的全新设计——统一的
(value, context)签名、上下文对象 API、auto-accessor关键字,以及六种装饰器类型的具体用法。理解它与 legacy 的本质区别。
TypeScript 5.0 起,标准装饰器不需要任何编译选项,直接写就行。你只需要确保 tsconfig.json 里 target 在 ES2015 以上(TS 7.0 的默认 target 就是 ES2015+),因为 auto-accessor 需要 class fields 支持。
全新的签名:(value, context)
标准装饰器抛弃了 legacy 那种”不同类型不同参数签名”的设计,统一成两个参数:
type Decorator = (
value: DecoratedValue,
context: DecoratorContext
) => void | ReplacementValue;
value:被装饰的东西。类就是类本身,方法是方法函数,属性是undefined。context:上下文对象,里面包含了元信息和钩子方法。
context 对象是所有装饰器共用的设计,不同装饰器类型里的字段会略有不同。
context 对象的通用字段
| 字段 | 类型 | 说明 |
|---|---|---|
kind | string | 装饰器类型:"class" / "method" / "getter" / "setter" / "field" / "accessor" |
name | string | symbol | 被装饰的成员名称(类装饰器时为 undefined) |
static | boolean | 是否是静态成员 |
private | boolean | 是否是私有成员(# 打头) |
addInitializer(fn) | (fn: () => void) => void | 注册初始化回调,在类构造时执行 |
注意 kind 的可取值共 6 种,意味着标准装饰器有 6 种类型——比 legacy 多了一种 accessor(对应 auto-accessor 关键字),少了一种参数装饰器。
类装饰器
类装饰器的 value 就是类本身(构造函数),context.kind 是 "class":
function sealed(value: Function, context: ClassDecoratorContext) {
if (context.kind === "class") {
Object.seal(value);
Object.seal(value.prototype);
}
}
@sealed
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
}
返回值:返回新类替换原类
function countInstances<T extends new (...args: any[]) => any>(
value: T,
context: ClassDecoratorContext
) {
let count = 0;
return class extends value {
constructor(...args: any[]) {
super(...args);
(this as any).instanceId = ++count;
}
} as any;
}
@countInstances
class Product {
name: string;
constructor(name: string) {
this.name = name;
}
}
const a = new Product("A");
const b = new Product("B");
console.log((a as any).instanceId); // 1
console.log((b as any).instanceId); // 2
addInitializer:类初始化钩子
context.addInitializer() 注册一个函数,在类定义完全结束后执行:
function register(name: string) {
return function (value: Function, context: ClassDecoratorContext) {
context.addInitializer(() => {
console.log(`组件 ${name} 注册完成`);
});
};
}
@register("UserCard")
class UserCard {}
// 输出:组件 UserCard 注册完成
方法装饰器
方法装饰器的 value 就是方法函数本身:
function log(
value: Function,
context: ClassMethodDecoratorContext
) {
if (context.kind === "method") {
const methodName = String(context.name);
return function (this: any, ...args: any[]) {
console.log(`调用 ${methodName}(${args.join(", ")})`);
return value.apply(this, args);
};
}
}
class Calculator {
@log
add(x: number, y: number) {
return x + y;
}
}
const calc = new Calculator();
calc.add(3, 5);
// 输出:调用 add(3, 5)
跟 legacy 的关键区别:没有 descriptor。你直接返回一个新函数替换原方法,不需要修改描述对象。更干净、更符合直觉。
addInitializer:替代构造函数里的 bind
以前你必须在 constructor 里写 this.method = this.method.bind(this),现在可以移到装饰器里:
function bound(
value: Function,
context: ClassMethodDecoratorContext
) {
if (context.private) {
throw new Error("不能绑定私有方法");
}
const methodName = context.name;
context.addInitializer(function (this: any) {
this[methodName] = this[methodName].bind(this);
});
}
addInitializer 的 this 指向类的实例,执行时机在构造函数里、早于属性初始化。
属性(field)装饰器
这是标准装饰器对 legacy 的一个重大改进。legacy 的属性装饰器拿不到值,标准装饰器虽然 value 也是 undefined,但可以返回一个初始化函数:
function double(
value: undefined,
context: ClassFieldDecoratorContext
) {
if (context.kind === "field") {
return function (initialValue: number) {
return initialValue * 2;
};
}
}
class C {
@double
x = 10;
}
const c = new C();
console.log(c.x); // 20
返回的初始化函数接收属性初始值,返回最终值。这解决了 legacy 属性装饰器”既不能读也不能写”的尴尬。
还可以通过 context.access 取出属性的存取器,在类外部读写:
let accessor: any;
function expose(
value: undefined,
context: ClassFieldDecoratorContext
) {
accessor = context.access;
}
class C {
@expose
name = "Alice";
}
const c = new C();
console.log(accessor.get(c)); // "Alice"
accessor.set(c, "Bob");
console.log(c.name); // "Bob"
getter / setter 装饰器
getter 装饰器的 context.kind 是 "getter",setter 是 "setter"。它们各自独立,允许分别装饰:
function validate(
value: Function,
context: ClassSetterDecoratorContext
) {
if (context.kind === "setter") {
return function (this: any, newVal: number) {
if (newVal > 100) {
throw new Error("值不能超过 100");
}
return value.call(this, newVal);
};
}
}
class C {
#score: number = 0;
@validate
set score(v: number) {
this.#score = v;
}
get score() {
return this.#score;
}
}
跟 legacy 不同,标准装饰器的 getter 和 setter 可以分别装饰——因为两个装饰器各自独立,不存在冲突。
auto-accessor:标准装饰器的独门武器
accessor 关键字是标准装饰器带来的全新语法。它把一个类属性变成自带 getter/setter 的”自动存取器”:
class C {
accessor x = 1;
}
// 等价于:
class C {
#x = 1;
get x() { return this.#x; }
set x(val) { this.#x = val; }
}
accessor 本身没啥特殊的,但它可以和装饰器配合,产生强大的组合效果:
function logged(
value: { get: () => unknown; set: (v: unknown) => void },
context: ClassAccessorDecoratorContext
) {
if (context.kind === "accessor") {
const { get, set } = value;
return {
get() {
const result = get.call(this);
console.log(`读取 ${String(context.name)} → ${result}`);
return result;
},
set(newVal) {
console.log(`设置 ${String(context.name)} ← ${newVal}`);
return set.call(this, newVal);
},
init(initialValue: unknown) {
console.log(`初始化 ${String(context.name)} = ${initialValue}`);
return initialValue;
},
};
}
}
class C {
@logged accessor x = 1;
}
const c = new C();
// 初始化 x = 1
c.x;
// 读取 x → 1
c.x = 42;
// 设置 x ← 42
accessor 装饰器返回的对象有三个可选方法:
get:拦截读取。set:拦截写入。init:修改初始值。
Tip
accessor也可以跟static和#一起用:static accessor x = 1、accessor #y = 2。
执行顺序
标准装饰器的执行顺序和 legacy 类似,但语义上更清晰:
- 评估阶段:先计算所有
@后面表达式的值(从上到下)。 - 应用阶段:先应用实例成员装饰器(方法 → getter → setter → field → accessor),然后是静态成员装饰器,最后是类装饰器。
同一成员上多个装饰器仍然是从下往上执行(后写的先执行)。
function d(name: string): any {
console.log(`评估 ${name}`);
return function () {
console.log(`应用 ${name}`);
};
}
@d("类")
class T {
@d("方法")
m() {}
}
// 评估 类
// 评估 方法
// 应用 方法
// 应用 类
六种标准装饰器速查
| 类型 | context.kind | value | 返回值 |
|---|---|---|---|
| 类 | "class" | 类本身 | Function(替换类) |
| 方法 | "method" | 方法函数 | Function(替换方法) |
| getter | "getter" | getter 函数 | Function(替换 getter) |
| setter | "setter" | setter 函数 | Function(替换 setter) |
| 属性 | "field" | undefined | 初始化函数 (initVal) => finalVal |
| accessor | "accessor" | { get, set } | { get?, set?, init? } |
注意:context.kind 的值是 "field",不是 "property"。它对应的是 class field 声明。
与 legacy 的本质差异
把两套装饰器放在一起看:
- 签名统一化:标准装饰器统一为
(value, context),不再因装饰目标不同而变来变去。 - 没有
descriptor:标准装饰器不需要操作属性描述对象,直接返回值替换即可。 - 没有参数装饰器:这是有意的设计决策——TC39 认为参数装饰器破坏了语言的”局部推理”能力。
- 属性装饰器可用:标准装饰器通过返回初始化函数解决了 legacy 属性装饰器”啥都干不了”的问题。
accessor关键字:标准装饰器的独家特性,配合 accessor 装饰器提供了最完整的拦截能力。
下一章我们聊聊实际项目里怎么选、怎么迁。