让引用数据实例的Getter自替换为返回值,当前装饰器方案是否最优?
我的应用会加载引用数据实例,部分实例的getter会依赖其他引用数据,示例如下:
class Person { name: string; cityId: number; get city(): City { return City.all.get(this.cityId)!; } }
由于引用数据永不改变,调用该getter始终会得到相同结果。因此我希望实现一个调用后能将自身替换为返回值的getter,并编写了如下装饰器:
export const selfReplacing = <T>( target: T, propertyKey: keyof T, descriptor: PropertyDescriptor ): any => { const oldGetter = descriptor.get!; descriptor.get = function (this: T) { const value = oldGetter.call(this); Object.defineProperty(this, propertyKey, { value, enumerable: true }); return value; }; };
使用方式如下:
class Person { name: string; cityId: number; @selfReplacing get city(): City { return City.all.get(this.cityId)!; } }
请问这是否是实现该需求的最佳方式?
你的这个装饰器实现思路完全贴合需求,而且逻辑简洁高效——既然引用数据是不可变的,首次调用后就没必要重复执行getter逻辑,直接把属性替换为静态值能节省后续的计算开销,在性能敏感场景里非常实用。
我们可以从优缺点和优化方向来拆解这个实现:
现有实现的优点
- 懒加载特性:只有当属性被实际访问时才会执行getter逻辑并替换为静态值,比在构造函数里提前初始化所有依赖属性更高效(毕竟有些属性可能永远不会被用到)
- 行为一致性:设置
enumerable: true确保属性在对象遍历、序列化等场景下的行为和原getter完全一致 - 核心逻辑直观:通过
Object.defineProperty直接覆盖原属性,没有多余的包装层,理解和维护成本低
可以优化的细节
类型安全性增强
当前装饰器返回类型是any,可以调整为更精确的PropertyDescriptor,让TypeScript能更好地做类型校验:export const selfReplacing = <T>( target: T, propertyKey: keyof T, descriptor: PropertyDescriptor ): PropertyDescriptor => { const oldGetter = descriptor.get!; descriptor.get = function (this: T) { const value = oldGetter.call(this); Object.defineProperty(this, propertyKey, { value, enumerable: true, configurable: true // 可选:如果后续需要修改属性配置可以加上 }); return value; }; return descriptor; };处理边缘返回值
虽然你提到引用数据永不改变,但City.all.get(this.cityId)仍有可能返回undefined,当前代码里的非空断言!会引发运行时错误。如果存在这种场景,可以在装饰器里添加判断逻辑,或者允许返回值为可选类型,避免强行断言的风险。明确属性可配置性
当前的Object.defineProperty没有设置configurable选项,默认是false,意味着后续无法修改这个属性的配置。如果你的场景不需要锁定属性,加上configurable: true会更灵活;如果确定不需要修改,也可以显式设置为false,明确语义。
是否是最佳方案?
在你的特定场景下,这个装饰器实现已经是非常优秀的选择了。
如果考虑替代方案:
- 构造函数提前初始化:会在实例创建时就执行所有getter逻辑,失去了懒加载的优势,适合所有属性都会被用到的场景
- Proxy代理:开销比直接覆盖属性大,而且需要额外包装实例,对于类实例来说,装饰器的方式更轻量直观
所以总结下来,你的实现完全适配需求,结合上面的小优化后会更健壮、更符合TypeScript最佳实践,是这个需求的理想解决方案。
内容的提问来源于stack exchange,提问作者Marco de Wit

