如何为TypeScript访问器方法创建enumerable装饰器
实现可枚举访问器的@enumerable装饰器方案
问题根源解析
你遇到的核心问题来自JavaScript属性枚举的规则和私有字段的访问限制:
- 原型属性与实例属性的区别:类的访问器默认定义在原型上,
Object.keys()/Object.getOwnPropertyNames()仅返回实例自身的可枚举属性,原型上的属性不会被包含——这就是你用装饰器修改原型访问器的enumerable后,Object.keys仍无法获取属性a的原因。 - for...in的特殊性:
for...in会遍历实例及其原型链上的所有可枚举属性,所以能看到原型上被标记为可枚举的a。 - Object.entries报错原因:当
Object.entries尝试遍历原型上的a访问器时,它会在原型对象上调用getter,而原型对象没有类实例的私有字段#privateVal1,因此抛出访问错误。
可行的装饰器实现方案
要让访问器能被Object.keys/Object.entries正确枚举,需要将原型上的访问器复制到实例自身并标记为可枚举。以下是结合属性装饰器和类装饰器的实现:
// 存储类需要枚举的属性名,用WeakMap避免内存泄漏 const enumerableProps = new WeakMap<Function, string[]>(); // 属性装饰器:标记需要枚举的访问器 function enumerable(value: boolean = true) { return function (target: any, propertyKey: string) { if (!value) return; const constructor = target.constructor; const props = enumerableProps.get(constructor) || []; if (!props.includes(propertyKey)) { props.push(propertyKey); } enumerableProps.set(constructor, props); }; } // 类装饰器:修改构造逻辑,实例化时复制访问器到实例 function EnumerableClass<T extends { new(...args: any[]): {} }>(constructor: T) { return class extends constructor { constructor(...args: any[]) { super(...args); const props = enumerableProps.get(constructor); if (props) { for (const prop of props) { const protoDescriptor = Object.getOwnPropertyDescriptor(constructor.prototype, prop); if (protoDescriptor) { // 将原型访问器复制到实例,显式设置enumerable为true Object.defineProperty(this, prop, { ...protoDescriptor, enumerable: true }); } } } } }; }
使用示例
@EnumerableClass class MyClass { #privateVal1: any; #privateVal2: any; constructor() { // 无需手动调用makeEnumerable } @enumerable(true) get a() { return this.#privateVal1; } set a(val: any) { this.#privateVal1 = val; } @enumerable(true) get b() { return this.#privateVal2; } set b(val: any) { this.#privateVal2 = val; } } const instance = new MyClass(); instance.a = 5; instance.b = 6; console.log(Object.keys(instance)); // ['a', 'b'] console.log(Object.entries(instance)); // [['a', 5], ['b', 6]] console.log(Object.getOwnPropertyNames(instance)); // ['a', 'b'] const keys = []; for (const key in instance) { keys.push(key); } console.log(keys); // ['a', 'b']
方案原理
- 属性装饰器:收集当前类中需要设置为可枚举的访问器属性名,存储在WeakMap中,避免对类构造函数造成额外内存占用。
- 类装饰器:继承原类并重写构造函数,在实例化时将收集到的访问器从原型复制到实例自身,并显式标记
enumerable: true。此时访问器的this指向实例,能正确访问私有字段,同时Object.keys等API能识别到这些实例自身的可枚举属性。
内容的提问来源于stack exchange,提问作者Harry Mustoe-Playfair
相关产品推荐
相关产品推荐

