JavaScript 静态私有字段:子类继承了方法,为什么仍读不到父类的计数

前天 3阅读

一个编号工具把累计数量放在静态私有字段里,父类调用一直正常。后来为了添加一种编号类型建立子类,子类虽然能找到原来的静态方法,一执行却抛出 TypeError。方法没有丢失,出问题的是方法里 this 指向了子类,而私有字段只装在声明它的那个类对象上。

方法能够继承,不代表私有存储一起继承

静态方法是类构造函数上的方法。通过 Child.next 调用继承来的方法时,this 仍是 Child。私有名称则由源码中的声明位置决定:父类方法里的井号字段始终指向父类声明的那一个私有名称,不会因为调用者换成子类,就自动寻找子类里拼写相同的字段。

下面保存为 demo.mjs,用 node demo.mjs 运行。实验只修改新建的演示类,不影响任何业务对象。第一组展示失败和字段身份,第二组明确选择整棵继承树共用一个计数。预期异常由断言接住,脚本仍会完整执行并输出验证结果。

import assert from 'node:assert/strict';

class Counter {
  static #count = 0;
  static next() { return ++this.#count; }
  static ownsCounter(value) { return #count in value; }
  static total() { return Counter.#count; }
}
class Child extends Counter {
  static #count = 100;
  static ownTotal() { return Child.#count; }
  static viaSuper() { return super.next(); }
}

assert.equal(Object.getPrototypeOf(Child), Counter);
assert.equal(Child.next, Counter.next);
assert.equal(Counter.next(), 1);
assert.equal(Counter.ownsCounter(Counter), true);
assert.equal(Counter.ownsCounter(Child), false);
assert.throws(() => Child.next(), TypeError);
assert.throws(() => Child.viaSuper(), TypeError);
assert.equal(Counter.total(), 1);
assert.equal(Child.ownTotal(), 100);
console.log('same inherited method:', Child.next === Counter.next);
console.log('parent private brand:', Counter.ownsCounter(Counter),
  Counter.ownsCounter(Child));
console.log('separate counts:', Counter.total(), Child.ownTotal());

class SharedCounter {
  static #count = 0;
  static next() { return ++SharedCounter.#count; }
}
class SharedChild extends SharedCounter {}
const shared = [SharedCounter.next(), SharedChild.next(),
  SharedCounter.next()];
assert.deepEqual(shared, [1, 2, 3]);
console.log('explicit shared counter:', shared.join(','));
console.log('all private-field checks passed');

同名字段和 super 都没有改变接收者

第一行输出 true,证明 Child.next 确实是继承来的同一个函数。第二行的 true 与 false 则说明父类的私有名称只存在于 Counter 上。这里使用类内部的井号加 in 检查私有元素,不能换成普通字符串属性检查;字符串形式的字段名属于另一套属性机制。

JavaScript 静态私有字段:子类继承了方法,为什么仍读不到父类的计数

AI生成概念示意图:继承关系连通了方法入口,但父类的私有抽屉仍属于原来的类对象。

子类也声明了一个名为 count 的私有字段,初始值是一百,但它并没有补上父类缺失的那一份。两个声明产生不同的私有名称。父类方法既不会读取它,也不会递归沿原型链找到父类字段;失败后,父类的一和子类的一百都保持原样。

viaSuper 展示另一种容易误判的修复。super 用来找到父类的方法,却没有把调用时的 this 换成父类,所以仍然失败。这里应该先确定存储归谁所有,再决定访问写法;只在调用前多写一个父类入口,并不能自动改变私有字段的归属。

共享计数应在代码里明确表达

第二组把读取目标固定为 SharedCounter,因此父类和子类依次调用得到一、二、三。这适合所有类型共享同一编号序列的设计:类名清楚表达了唯一的存储位置。但它也意味着新增子类不会自动拥有独立计数,不能因为错误消失就默认业务需求已经满足。

如果每种子类都要独立累计,就需要另外建模,例如每个具体类声明自己的字段与访问方法,或由父类管理一张以类对象为键的状态表。两者分别强调显式定义和集中管理,应根据扩展方式选择,而不是期待静态私有字段表现得像其他语言的受保护静态变量。

测试继承工具时,至少分别调用声明类、直接子类和更深一层子类,并检查计数是否按设计共享。错误断言只依赖异常类型,避免把某个引擎的报错文案写死。还要验证失败没有推进编号,这样才能区分字段访问失败与业务操作已经部分完成。

本文讨论静态字段,也就是存储在类对象上的状态。实例私有字段会在实例构造过程中安装,不能直接套用“子类对象没有父类静态字段”来推断实例行为。阅读报错时先标明访问的是类还是实例,再追踪声明位置和实际接收者,定位会清楚得多。

资料核对日期:2026年10月2日(北京时间)。示例在 Node.js v24.19.0 中独立执行并通过断言。

官方参考:ECMAScript 私有字段读取与字段定义;ECMAScript 类定义求值。

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。