JavaScript 类字段初始化:super 调过子类方法,值为什么又被覆盖

10-01 3阅读

父类构造器调用了一个子类重写的方法,日志也显示赋值成功;可 new 返回后,字段又变回初始值。这个问题通常不是有另一个异步任务在改对象,而是子类实例字段尚未初始化。构造阶段的调用顺序,和阅读类声明时的视觉顺序并不相同。

派生类的实例字段在父类构造器返回之后、super 调用完成之前初始化。父类构造器执行期间,通过 this 访问原型方法,已经可能找到子类的重写版本;方法能被找到,并不表示它依赖的子类字段已经存在。

用一份事件表观察覆盖发生在哪里

把代码保存为 demo.mjs,用 node demo.mjs 执行。本文使用 Node.js v24.19.0,测试原生 JavaScript 类,不经过 TypeScript 或 Babel 转换。事件表逐步记录值,比在最后打印一次对象更适合定位初始化顺序。

JavaScript 类字段初始化:super 调过子类方法,值为什么又被覆盖

AI概念示意图:用抽象图形说明本文主题,不代表真实界面或运行结果。

import assert from 'node:assert/strict';
const events = [];
class Base {
  constructor() { events.push('base'); this.prepare(); }
  prepare() {}
}
class Child extends Base {
  value = (events.push('field'), 'field');
  prepare() {
    events.push(`method:${this.value}`);
    this.value = 'from-method';
  }
  constructor() {
    super();
    events.push(`body:${this.value}`);
  }
}
const item = new Child();
assert.equal(item.value, 'field');
assert.deepEqual(events, ['base', 'method:undefined', 'field', 'body:field']);
console.log(events.join('|'));
class SafeBase {
  constructor() { this.baseReady = true; }
}
class SafeChild extends SafeBase {
  value = 'field';
  constructor() { super(); this.prepare(); }
  prepare() { this.value = `ready:${this.value}`; }
}
const safe = new SafeChild();
assert.equal(safe.baseReady, true);
assert.equal(safe.value, 'ready:field');
console.log(safe.value);

原型方法可用,实例字段还没准备好

第一行依次出现 base、method:undefined、field 和 body:field。父类先进入构造器,随即调用子类 prepare。此时 value 还没有由子类字段声明创建,读取结果是 undefined;方法写入 from-method 之后,字段初始化器又把它定义成 field。最后子类构造器正文才读到 field。

不要把子类字段声明理解成自动移到 super 之前的赋值语句。派生构造器在正常完成 super 之前也不能随意使用 this。给字段补一个默认值,可能只是增加一次更晚的写入,并没有修复父类过早调用方法的问题。

让依赖子类状态的工作晚一点开始

示例的 SafeBase 构造器只建立父类自己的状态。SafeChild 的字段完成初始化后,再在构造器正文调用 prepare,结果变成 ready:field。第二组断言同时核对父类状态和最终字段,避免修好子类时遗漏基础初始化。

实际项目可以把这一步写成明确的初始化方法或工厂函数:先完成对象构造,再执行依赖完整实例的工作。选择哪一种取决于调用者是否需要拿到“已经准备好”的对象。若初始化可能异步失败,工厂返回 Promise 往往更容易表达失败边界,但不要把未准备好的实例提前交给其他组件。

排查时还要分清另外两种情况

没有初始化表达式的公开字段声明,仍会创建值为 undefined 的字段,不能靠删除等号右边来保证父类赋值保留。私有字段则更严格:父类过早调用的子类方法如果读取尚未安装的私有字段,可能直接抛出 TypeError,而不只是读到 undefined。

这里演示的是公开实例字段与原型方法,不涵盖静态字段、构造器显式返回其他对象等额外机制。只要保持例子的边界,就能把异常来源缩小到清楚的四个阶段,再逐步加入项目中的装饰器、访问器或框架生命周期。

如果线上代码经过转译,应检查真正运行的产物和编译选项。不同的历史转换方式可能把字段实现为赋值或属性定义,涉及父类 setter 时结果尤其值得另测。不要仅凭源码看起来一样,就把原生实验结论扩展到所有旧构建产物。

最终修复的验收重点是:父类构造期间不依赖未就绪的子类状态,最终实例具有预期字段,初始化只在设计的位置发生一次。为事件顺序保留一个小断言,能比单纯核对最终值更早发现后续重构重新引入的过早调用。

参考资料

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