为何原型含私有属性类实例的对象访问私有成员会报错?
问题:私有成员访问异常与类实例克隆方法
问题示例代码
class Klass { #priv = 42 get pub() { return this.#priv } } // 以下两种情况正常返回42 console.log(new Klass().pub); console.log((new class extends Klass {}).pub) // 但此代码抛出错误:"TypeError: Cannot read private member #priv from an object whose class did not declare it" console.log(Object.create(new Klass()).pub) // 报错 // 尽管以下判断结果为true console.log(Object.create(new Klass()) instanceof Klass) // true console.log(Object.getPrototypeOf(Object.create(new Klass())).pub) // 42
我原本以为只要原型链中存在Klass的真实实例,访问Object.create(new Klass()).pub就不会报错。请问这是设计如此吗?若如此,原因是什么?另外,克隆类似Klass这类任意类实例的正确方法是什么?
背景场景
我在使用Vitest测试时遇到该问题,测试代码如下:
import { it, expect } from 'vitest'; class Klass { #priv = 42; get pub() { return this.#priv; } } it('works', () => { expect(new Klass()).toMatchObject({ make_this_test_fail: 'yup', pub: 42, }); });
测试失败后未正常显示属性缺失的差异,而是抛出了私有成员访问错误。经排查发现,Vitest的deepClone方法使用Object.create创建类实例导致该问题。
解答
1. 这是JavaScript私有字段的设计特性
没错,这就是ECMAScript私有字段(#开头的成员)的既定规则。私有字段的访问逻辑并非基于原型链查找,而是严格绑定对象自身的类声明:
- 只有当对象是由声明了该私有字段的类(或其派生类)直接实例化出来的,才能访问这个私有字段。
Object.create(new Klass())创建的对象,只是把Klass实例作为原型,但这个对象本身并不是Klass类的实例——它没有在自身结构中持有#priv字段,也没有被Klass类的构造函数初始化过,所以即使原型链上有带私有字段的实例,也无法通过这个对象访问私有字段。
instanceof返回true只是因为它只检查原型链中是否存在Klass的原型,不关心对象本身是否持有类的私有成员。
2. 克隆含私有字段类实例的正确方法
针对包含私有字段的类,没有通用的“万能克隆法”,但可以根据场景选择以下方案:
方案一:类自身实现克隆方法
在类内部添加clone方法,利用类的构造函数和内部对私有字段的访问权限来复制:
class Klass { #priv = 42 get pub() { return this.#priv } clone() { const newInstance = new Klass(); // 如果私有字段需要自定义复制逻辑,在这里处理 newInstance.#priv = this.#priv; return newInstance; } } const original = new Klass(); const cloned = original.clone(); console.log(cloned.pub); // 42
方案二:使用结构化克隆(Structured Clone)
对于支持结构化克隆的场景(比如浏览器环境、Node.js),可以用structuredClone API,它能正确复制类实例的私有字段:
const original = new Klass(); const cloned = structuredClone(original); console.log(cloned.pub); // 42
注意:结构化克隆有局限性,比如无法复制函数、Symbol属性等,但对于普通带私有字段的类实例适用。
方案三:针对测试场景的临时处理(以Vitest为例)
如果是Vitest的toMatchObject导致的问题,可以避开深度克隆,改用其他断言方式:
- 单独断言公共属性:
expect(new Klass().pub).toBe(42); - 手动提取公共属性后断言:
expect({ pub: new Klass().pub }).toMatchObject({ pub: 42, make_this_test_fail: 'yup' }); - 升级Vitest版本,部分新版本已经修复了私有字段克隆的问题。
内容的提问来源于stack exchange,提问作者Bartosz Gościński
相关产品推荐
相关产品推荐

