装饰器 Stage3 核心用法
装饰器提案相关内容请见:关于装饰器提案
核心用法
type Decorator = (value: Input, context: {
kind: string;
name: string | symbol;
access: { get?(): unknown; set?(value: unknown): void };
static?: boolean;
private?: boolean;
addInitializer(initializer: () => void): void;
}) => Output | void;
装饰器函数接收两个参数:
- value:被装饰的目标(类、方法、属性等)本身
- context:上下文对象,包含以下属性:
- kind:装饰器类型"class"|"method"|"getter"|"setter"|"field"|"accessor"
- name:被装饰元素的名称(字符串或Symbol)
- access:包含get/set方法,用于访问被装饰元素
- static:是否为静态成员
- private:是否为私有成员
- addInitializer(initializer):用于添加初始化逻辑的函数
装饰器类型
类装饰器
-
作用:修改或替换类构造函数
-
参数说明:
type ClassDecorator = (value: Function, context: { kind: "class"; name: string | undefined; addInitializer(initializer: () => void): void; }) => Function | void; -
用法示例:添加日志功能
function logged(value, { kind, name }) { if (kind === "class") { return class extends value { constructor(...args) { super(...args); console.log(`constructing an instance of ${name} with arguments ${args.join(", ")}`); } } } // ... } @logged class C {} new C(1); // constructing an instance of C with arguments 1 -
语法脱糖(desugars):
class C {} C = logged(C, { kind: "class", name: "C", }) ?? C; new C(1);
类方法装饰器
-
作用:包装或替换类方法
-
参数说明:
type ClassMethodDecorator = (value: Function, context: { kind: "method"; name: string | symbol; access: { get(): unknown }; static: boolean; private: boolean; addInitializer(initializer: () => void): void; }) => Function | void; -
用法示例:
function logged(value, { kind, name }) { if (kind === "method") { return function (...args) { console.log(`starting ${name} with arguments ${args.join(", ")}`); const ret = value.call(this, ...args); console.log(`ending ${name}`); return ret; }; } } class C { @logged m(arg) {} } new C().m(1); // starting m with arguments 1 // ending m-
this的指代:在装饰器返回的函数内部,this就是当前实例(即调用该方法的对象实例)。
-
value.call(this, ...args)的作用:- 恢复原始方法调用:
- value是被装饰的原始方法m
.call()显示设置方法执行时的this上下文
- 参数传递:
...args将接收到的参数原样传递给原始方法- 确保参数传递不丢失
- 返回值处理:
const ret = ...捕获原始方法的返回值return ret确保装饰后方法返回原始结果
- 这样就使得在保留原方法功能的基础上,添加额外的日志行为(在方法开始和结束时打印日志),而不会改变原方法的逻辑。
- 恢复原始方法调用:
-
当我们调用
new C().m(1)时,实际上执行的是装饰器返回的新函数。过程如下:- 先打印开始日志:starting m with arguments 1。
- 然后调用原始方法m,将this(即当前实例)作为上下文,并传入参数1。这样在原始方法内部,this也是指向实例,并且参数arg为1。
- 原始方法执行,打印:方法内部,参数:1。
- 接着打印结束日志:ending m。所以,this确保了原始方法能够访问实例的属性和其他方法,而value.call则是调用原始方法的关键。
-
如果使用
value(...args)代替value.call(this, ...args),会导致什么问题?function brokenLogged(value, { kind, name }) { if (kind === "method") { return function (...args) { console.log(`starting ${name}`); // 错误!丢失 this 绑定 const ret = value(...args); console.log(`ending ${name}`); return ret; }; } } class Example { value = 10; @brokenLogged getValue() { return this.value; // 这里 this 会变成 undefined! } } new Example().getValue(); // 抛出错误:Cannot read properties of undefined
-
-
语法脱糖(desugars):
class C { m(arg) {} } C.prototype.m = logged(C.prototype.m, { kind: "method", name: "m", static: false, private: false, }) ?? C.prototype.m;
类属性装饰器
-
作用:拦截属性初始化(Stage 3重大变化)。这种类型的装饰器不会接收到 value(即value固定为undefined),但是可以通过初始化函数来拿到初始数据,并且还可以返回一个新的数据。
- 和访问器装饰器、方法装饰器区别,属性装饰器的第一个参数为undefined。属性装饰器可以返回一个初始化函数,返回的初始化函数的入参为原始属性值,返回值为替代原始的属性值。
-
参数说明:
type ClassFieldDecorator = (value: undefined, context: { kind: "field"; name: string | symbol; access: { get(): unknown, set(value: unknown): void }; static: boolean; private: boolean; }) => (initialValue: unknown) => unknown | void; -
用法示例:
function logged(value, { kind, name }) { if (kind === "field") { return function (initialValue) { console.log(`initializing ${name} with value ${initialValue}`); return initialValue; }; } // ... } class C { @logged x = 1; } new C(); // initializing x with value 1 -
语法脱糖(desugars):
let initializeX = logged(undefined, { kind: "field", name: "x", static: false, private: false, }) ?? (initialValue) => initialValue; class C { x = initializeX.call(this, 1); }
类访问器装饰器
-
作用:专门用于装饰类中的 getter 和 setter 方法(是一种特殊的方法装饰器)。
-
参数说明:
type ClassGetterDecorator = (value: Function, context: { kind: "getter"; name: string | symbol; access: { get(): unknown }; static: boolean; private: boolean; addInitializer(initializer: () => void): void; }) => Function | void; type ClassSetterDecorator = (value: Function, context: { kind: "setter"; name: string | symbol; access: { set(value: unknown): void }; static: boolean; private: boolean; addInitializer(initializer: () => void): void; }) => Function | void; -
用法示例:
function logged(value, { kind, name }) { if (kind === "getter" || kind === "setter") { return function (...args) { console.log(`starting ${name} with arguments ${args.join(", ")}`); const ret = value.call(this, ...args); console.log(`ending ${name}`); return ret; }; } } class C { @logged set x(arg) {} } new C().x = 1 // starting x with arguments 1 // ending x -
语法脱糖(desugars):
class C { set x(arg) {} } let { set } = Object.getOwnPropertyDescriptor(C.prototype, "x"); set = logged(set, { kind: "setter", name: "x", static: false, private: false, }) ?? set; Object.defineProperty(C.prototype, "x", { set }); -
和普通的方法装饰器的区别:
-
上下文标识不同
- 普通方法:
kind === "method" - 访问器方法:
kind === "getter"/"setter"
- 普通方法:
-
参数差异
// 普通方法可能有多个参数 @logged transfer(amount, toAccount) { ... } // getter 无参数 @logged get balance() { ... } // setter 单参数 @logged set balance(value) { ... }
-
新的类元素
自动访问器
-
作用:通过accessor修饰,自动生成私有属性和关联的 getter/setter 方法
-
用法示例:
class C { accessor x = 1; } // 等同于 class C { #x = 1; get x() { return this.#x; } set x(val) { this.#x = val; } } // 也可以定义静态 class C { static accessor x = 1; accessor #y = 2; }
自动访问器装饰器
-
作用:与属性装饰器不同,自动访问器装饰器接收一个value,value是一个包含类原型 (或在静态自动访问器的情况下类本身) 上定义的 get 和 set 访问器的对象。装饰器可以包装这些值并返回一个新的 get / set,从而允许装饰器拦截对该属性的访问。
- 此外,自动访问器可以返回一个 init 函数,该函数可用于更改属性的初始值,类似于属性装饰器。
- 如果返回一个对象,但省略了其中任何值,则省略值的默认行为是使用原始行为。如果返回的值类型不是包含这些属性的对象,则会抛出一个错误。
-
参数说明:
type ClassAutoAccessorDecorator = ( value: { get: () => unknown; set(value: unknown) => void; }, context: { kind: "accessor"; name: string | symbol; access: { get(): unknown, set(value: unknown): void }; static: boolean; private: boolean; addInitializer(initializer: () => void): void; } ) => { get?: () => unknown; set?: (value: unknown) => void; init?: (initialValue: unknown) => unknown; } | void; -
用法示例:
function logged(value, { kind, name }) { if (kind === "accessor") { let { get, set } = value; return { get() { console.log(`getting ${name}`); return get.call(this); }, set(val) { console.log(`setting ${name} to ${val}`); return set.call(this, val); }, init(initialValue) { console.log(`initializing ${name} with value ${initialValue}`); return initialValue; } }; } // ... } class C { @logged accessor x = 1; } let c = new C(); // initializing x with value 1 c.x; // getting x c.x = 123; // setting x to 123 -
语法脱糖(desugars):
class C { #x = initializeX.call(this, 1); get x() { return this.#x; } set x(val) { this.#x = val; } } let { get: oldGet, set: oldSet } = Object.getOwnPropertyDescriptor(C.prototype, "x"); let { get: newGet = oldGet, set: newSet = oldSet, init: initializeX = (initialValue) => initialValue } = logged( { get: oldGet, set: oldSet }, { kind: "accessor", name: "x", static: false, private: false, } ) ?? {}; Object.defineProperty(C.prototype, "x", { get: newGet, set: newSet });
关于addInitializer
通过 addInitializer 可以添加额外的初始化逻辑,这些逻辑的执行时机取决于所修饰对象的类型:
-
修饰类:在类被完全定义并且所有静态字段都被初始化之后运行。
-
修饰类元素(方法/属性) :在实例化期间(执行构造函数)运行,但是在实例字段被初始化之前。
-
修饰静态元素:类定义的过程中,在普通字段定义完成后,并且在静态字段定义之前执行。
修饰类定义
-
用法示例:举例 @customElement
- 实现了将 JavaScript 类转换为可在 HTML 中使用的自定义标签的功能。
- 将自定义元素名称(如 'my-element')与 类(如 MyElement)关联。
- 允许在 HTML 中使用 标签。
function customElement(name) { return (value, { addInitializer }) => { addInitializer(function() { customElements.define(name, this); // Web API 用于在浏览器中注册自定义元素 }); } } @customElement('my-element') class MyElement extends HTMLElement { } - 在类完全定义后(类装饰器的初始化时机),回调函数被调用,此时 this = 类构造函数 MyElement,执行 customElements.define('my-element', MyElement)。
-
语法脱糖(desugars):
class MyElement { } let initializersForMyElement = []; MyElement = customElement('my-element')(MyElement, { kind: "class", name: "MyElement", addInitializer(fn) { initializersForMyElement.push(fn); }, }) ?? MyElement; for (let initializer of initializersForMyElement) { initializer.call(MyElement); }
修饰类元素
-
用法示例:举例 @bound
-
作用是自动将类的方法绑定到类的实例上。这样,即使将方法作为函数提取出来单独调用,它内部的
this仍然指向类的实例,而不是丢失上下文(即不会变成undefined或全局对象等)。function bound(value, { name, addInitializer }) { // 1. 注册初始化逻辑 addInitializer(function () { // 3. 在实例创建时执行 // this指当前创建的实例,name指被装饰的方法名('m') // bind(this) 创建新函数,永久绑定this到当前实例,原始函数的功能不变,但this上下文固定 // 将绑定后的新函数赋值回实例属性 this[name] = this[name].bind(this); }); } class C { message = "hello!"; @bound m() { // 2. 装饰器应用于方法 console.log(this.message); } } let { m } = new C(); m(); // hello! -
问题背景:为什么要有这个@bound装饰器?为了解决this丢失问题
- 当方法从实例中提取后调用,this 指向 undefined(严格模式下)
- 需要手动绑定:
instance.m.bind(instance)class C { message = "hello!"; m() { console.log(this.message); } } const instance = new C(); const extractedMethod = instance.m; extractedMethod(); // 错误:Cannot read property 'message' of undefined
-
执行时序:
-
类定义阶段:装饰器
@bound被应用,装饰器函数接收到方法m和上下文信息,addInitializer注册一个回调函数。 -
实例创建阶段(new C()):
// 伪代码表示实例化过程 const instance = new C(); // 执行所有通过 addInitializer 注册的回调 for (const initializer of initializers) { initializer.call(instance); // this = 当前实例 } -
回调函数执行:
function () { this[name] = this[name].bind(this); // 等价于: // instance.m = instance.m.bind(instance); }
-
-
-
语法脱糖(desugars):
class C { constructor() { for (let initializer of initializersForM) { initializer.call(this); } this.message = "hello!"; } m() {} } let initializersForM = [] C.prototype.m = bound( C.prototype.m, { kind: "method", name: "m", static: false, private: false, addInitializer(fn) { initializersForM.push(fn); }, } ) ?? C.prototype.m;
关于Access
-
作用:上文中的例子都属于装饰器的替换和初始化能力,还没看到装饰器的访问能力。下面的例子是依赖注入装饰器,通过元数据能力使用access向实例注入数据。可以通过装饰器向实例注入数据。
-
用法示例:
const INJECTIONS = new WeakMap(); // 存储每个类需要的依赖信息 // 依赖注入创建器 function createInjections() { const injections = []; // injectable 类装饰器,标记类为可注入的 function injectable(Class) { // 将当前收集的注入点保存到INJECTIONS INJECTIONS.set(Class, injections); } // inject 属性装饰器工厂,标记需要注入的属性 function inject(injectionKey) { // 返回装饰器函数 return function applyInjection(v, context) { // 收集注入点信息,其中context.access.set是Stage3装饰器中提供的属性设置器 injections.push({ injectionKey, set: context.access.set }); }; } return { injectable, inject }; } // 依赖容器 class Container { registry = new Map(); // 注册依赖 register(injectionKey, value) { this.registry.set(injectionKey, value); } // 查找依赖 lookup(injectionKey) { return this.registry.get(injectionKey); } // 创建类实例并注入依赖 create(Class) { let instance = new Class(); // 1. 创建实例 // 2. 获取类的注入点配置 const classInjections = INJECTIONS.get(Class) || []; // 3. 执行依赖注入 for (const { injectionKey, set } of classInjections) { set.call(instance, this.lookup(injectionKey)); // 查找依赖、注入依赖 } return instance; } } class Store {} const { injectable, inject } = createInjections(); @injectable class C { @inject('store') store; } let container = new Container(); let store = new Store(); container.register('store', store); let c = container.create(C); c.store === store; // true- 以上代码实现了一个简单的依赖注入容器,利用装饰器来标记需要注入的类和属性,然后在创建实例时完成依赖注入。它使用了
WeakMap来存储类的元数据(需要注入的属性信息),避免内存泄漏。整个流程如下:-
使用
@injectable标记类(在装饰器执行时,将该类需要注入的属性信息存入INJECTIONS)。 -
使用
@inject(key)标记需要注入的属性(在装饰器执行时,将属性信息收集到闭包数组injections中,然后被injectable装饰器存入INJECTIONS)。 -
容器
Container负责注册依赖项,并在创建实例时查找依赖并设置到实例上。这种模式在需要解耦和测试的场景中非常有用,例如将服务注入到组件中。 -
-
- 以上代码实现了一个简单的依赖注入容器,利用装饰器来标记需要注入的类和属性,然后在创建实例时完成依赖注入。它使用了
相关链接
- Typescript v5.0 beta: devblogs.microsoft.com/typescript/…
- Stage3 装饰器提案: github.com/tc39/propos…
- Stage1 到 Stage3的重点变化: mp.weixin.qq.com/s?__biz=Mzk…
- 常见装饰器: github.com/jayphelps/c…