React中JS类私有字段克隆失效,如何实现不可变模式?
我在React应用里用带私有字段的JS类(不想到处用普通Object实例),但React的不可变状态要求修改时必须克隆对象。用getter/setter访问私有字段时,发现Firefox、Chrome和Node里私有字段都没法被克隆,反而React的Jest测试环境里代码能正常跑,出现假阳性结果。如果没解决办法,只能放弃封装安全性,改用下划线前缀的伪私有字段。
我的克隆函数:
const objclone = obj => { const cloned = Object.assign( Object.create( Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj), ), obj, ); return cloned; };
这个函数能克隆getter、setter和对象属性,但碰到私有字段就失效。
示例代码:
class C { #priv; constructor() { this.#priv = 'priv-value'; } get priv() { return this.#priv; } } const obj = new C(); console.log("obj.priv", obj.priv); const cloned = objclone(obj); console.log("cloned.priv", cloned.priv);
访问cloned.priv时的错误:
- Firefox:
Uncaught TypeError: can't access private field or method: object is not the right class
- Chrome和Node:
Uncaught TypeError: Cannot read private member #priv from an object whose class did not declare it
JS的私有字段(#xxx)是绑定到类本身的,不是对象实例的属性。常规克隆方法(比如Object.assign、Object.create)只能复制公开的属性和原型,根本无法访问或复制私有字段。克隆出来的对象虽然原型指向C,但没有初始化私有字段#priv,所以调用getter时会报错,因为当前对象没有该私有字段的绑定。
至于Jest里的假阳性,大概率是测试环境对私有字段的检查机制不严格,或者使用了未完全兼容私有字段的JS引擎/转译配置,导致错误没被触发。
1. 在类内部实现自定义克隆方法
既然只有类内部能访问私有字段,就在类里加一个clone方法,手动复制私有字段的值:
class C { #priv; constructor() { this.#priv = 'priv-value'; } get priv() { return this.#priv; } // 自定义克隆方法 clone() { const newInstance = new C(); // 类内部直接访问私有字段复制值 newInstance.#priv = this.#priv; return newInstance; } }
然后修改通用克隆函数,优先调用对象的自定义clone方法:
const objclone = obj => { // 如果对象有自定义克隆方法,优先调用 if (typeof obj.clone === 'function') { return obj.clone(); } // 原逻辑处理无自定义克隆的对象 const cloned = Object.assign( Object.create( Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj), ), obj, ); return cloned; };
2. 批量处理类的私有字段(如果有多个)
如果类里有多个私有字段,可以在构造函数里接收初始值,克隆时把当前实例的私有字段值传给新实例:
class C { #priv1; #priv2; constructor(priv1 = 'default1', priv2 = 'default2') { this.#priv1 = priv1; this.#priv2 = priv2; } get priv1() { return this.#priv1; } get priv2() { return this.#priv2; } clone() { // 把当前实例的私有字段值传给新实例的构造函数 return new C(this.#priv1, this.#priv2); } }
3. 避免伪私有字段(下划线前缀)
除非万不得已,不要用_priv这种伪私有字段——它本质还是公开属性,任何地方都能修改,完全失去了封装意义。优先用上面的自定义克隆方法解决问题。
内容的提问来源于stack exchange,提问作者Radu C

