JS/TS中Getter与Setter引发的抽象泄漏问题及优化咨询
我正在开发一款游戏,为避免绑定特定框架,给图形引擎写了抽象层,现在要适配three.js。我用gl-matrix做向量运算(采用vec3类型化数组),但three.js有自己的向量类(格式为{x:number, y:number, z:number})。
我的3D对象接口定义如下:
interface Thing{ ... position:vec3 }
对应的three.js实现类:
class ThreeJsThing implements Thing{ ... set position(p:vec3){ this.mesh.position.set(p[0], p[1], p[2]) // [,,] -> {xyz} } get position(){ let p = this.mesh.position return vec3.fromValues(p.x, p.y, p.z) // {xyz} -> [,,] } }
执行以下操作时会出现问题:
let myThing = new ThreeJsThing() myThing.position[1] = 5 // 仅修改了Getter返回的临时副本
由于gl-matrix为追求性能使用输出参数,内部会执行数组赋值操作,问题更突出:
vec3.add(myThing.position, a, b)
我已知临时解决方案,但其他开发者可能遭遇这种静默失败。从代码设计角度,我不清楚问题出在哪些反模式或不良设计上,认为ThreeJsThing已尽可能贴合接口规范。我的疑问:
- 是否应该避免使用Getter和Setter?
- Getter和Setter是否不应进行拷贝,仅接受、返回并使用同类型引用?
- 输出参数是否不被推荐(但
myThing.position[1] = 5的问题仍存在)?
补充:希望将此视为通用问题,明确其他场景如何规避此类问题。核心问题似乎是Getter不应返回可变副本,因为外部看起来它是可写入的常规成员。想了解Getter和Setter还有哪些需要注意的陷阱?
1. 是否应该避免使用Getter和Setter?
不是必须完全避免,但要明确Getter/Setter的语义。当属性背后需要做类型转换、状态同步(比如同步到three.js mesh)这类逻辑时,Getter/Setter是合理选择,但必须警惕它和普通属性的行为差异——普通属性返回自身引用,而你的Getter返回新创建的副本,这就是静默失败的根源。
如果接口定义position: vec3给外部的预期是「可以直接修改这个向量的元素」,那用Getter/Setter就会违背预期,这种场景下不如直接暴露修改位置的方法,比如setPosition(x: number, y: number, z: number)或updatePosition(vec: vec3),明确告知外部:修改位置需要调用方法,而非直接操作属性。
2. Getter和Setter是否不应进行拷贝,仅接受、返回并使用同类型引用?
这取决于抽象层的目标:
- 如果要完全贴合接口的
vec3类型,且外部预期可以直接操作向量引用,那应该在ThreeJsThing内部维护一个vec3类型的私有变量,Getter返回这个私有变量的引用,Setter接收vec3后同步更新私有变量和three.js的mesh位置。这样外部修改position的元素会直接作用于内部变量,再通过机制同步到three.js(比如每一帧更新,或修改时触发同步)。但要注意引用共享带来的意外修改——外部持有引用随意修改可能导致内部状态与three.js不同步,除非每次同步都主动从内部变量更新mesh。 - 如果必须和three.js原生向量绑定(不想维护额外vec3变量),Getter返回副本的做法本身没问题,但必须明确告知外部:这个属性是只读的,修改需通过Setter或专用方法。比如把接口的
position改成只读,再添加修改方法,或者用命名区分,比如getPosition(): vec3和setPosition(p: vec3),而非属性的Getter/Setter。
3. 输出参数是否不被推荐?
gl-matrix用输出参数是为了性能(减少内存分配),本身合理,但它的使用场景是操作已知的、可修改的引用。你的问题在于myThing.position返回的是临时副本,把它作为输出参数传入,修改的只是副本,不会同步到内部状态。
输出参数本身不是反模式,但需要外部传入可持久化的引用。如果抽象层要兼容这种用法,要么内部维护一个vec3引用供外部操作,要么提供方法让外部传入自己的vec3来获取当前位置,比如getPosition(out: vec3): vec3,这样外部可以复用自己的数组,也不会产生误解。
Getter/Setter的常见陷阱
- 返回可变对象的副本:如同你的场景,外部以为修改的是属性本身,实际只是临时副本,导致静默失败。如果必须返回副本,应把属性标记为只读,或用明确方法替代。
- 隐藏昂贵操作:如果Getter内部有复杂计算、IO或大量内存分配,外部可能误以为访问属性是廉价操作,频繁调用导致性能问题。
- 违背原子性:比如Setter内部做多个操作,若其中一个失败,可能导致状态不一致。比如同步three.js mesh位置时,若mesh不存在,会导致内部状态与mesh状态不匹配。
- 和普通属性行为不一致:比如普通属性可以用
delete,但Getter/Setter不行;或者给属性赋值undefined时,Setter的处理逻辑与普通属性不同,外部容易踩坑。 - 循环依赖:比如Getter内部访问另一个属性,而那个属性的Getter又访问当前属性,导致无限循环。
内容的提问来源于stack exchange,提问作者mqnc

