JS ES类顶部声明字段时多态对象初始化行为异常问题咨询
ES类字段初始化时序引发的多态异常问题
问题背景
正在开发一款浏览器标签页管理扩展时,发现在类顶部集中声明类字段的场景下,构造函数阶段的多态调用会出现属性值不符合预期的问题,可通过最小示例稳定复现。
复现代码
首先定义基类View:
class View { _viewModel; constructor(viewModel) { this._viewModel = viewModel; this.init(); } init() { } }
再定义继承自View的派生类TabView:
class TabView extends View { _title; constructor(viewModel) { super(viewModel); } init() { this.title = "test"; } get title() { return this._title; } set title(value) { this._title = value; } }
执行测试脚本触发逻辑:
const tabView = new TabView("model"); console.log(tabView.title);
现象对比
按照正常多态逻辑,预期的执行链路为:
- 进入TabView构造函数
- 通过
super()触发父类View构造函数执行 - 父类构造函数调用被TabView重写的
init()方法
预期实例属性值:
_viewModel: "model"_title: "test"
实际运行得到的实例属性值:
_viewModel: "model"_title: "undefined"
调试定位到的现象
- 移除TabView类顶部声明的
_title字段后,所有行为完全符合预期 - 当前问题在最新版Firefox、Microsoft Edge浏览器中均可稳定复现
因为习惯将类字段集中声明在类的顶部,需要确认该行为是ES规范定义的标准表现,还是会在后续版本被修正的实现bug。
问题解答
该行为是ES规范明确定义的标准表现,不属于浏览器实现bug,也不会在后续ES版本中被修正。
核心原因来自ES对类字段初始化时序的强制规定:
- 基类的类字段,会在基类构造函数的最开头(所有手写构造逻辑执行前)完成初始化
- 派生类的类字段,必须等
super()调用全部执行完成、从父类构造函数返回后,才会在实例上完成初始化挂载
对应到上述复现场景,完整的执行时序拆解如下,可精准定位异常来源:
- 执行
new TabView("model"),首先进入TabView构造函数 - 第一行执行
super(viewModel),流程跳转到父类View的构造函数上下文 - View构造函数给
this._viewModel赋值为传入的"model",随后调用this.init() - 此时因为
super()调用还没走完,派生类TabView声明的_title字段尚未完成初始化,init()里触发set title逻辑给this._title = "test"时,会在实例上创建一个普通的可配置自有属性,值为"test" - View构造函数执行完毕,流程回到TabView的构造函数上下文,JS引擎立刻执行TabView的类字段初始化逻辑:在实例上定义
_title属性,初始值为类字段声明的默认值undefined——这一步直接把之前init()里赋的"test"完全覆盖 - 所有类字段初始化完成后,TabView构造函数才正式走完执行流程、返回实例
整个过程中this的指向完全正确,不存在“init被绑到父类实例上”的问题,只是类字段的初始化时机晚于父类构造函数里的init调用,导致之前的赋值被后续的字段初始化覆盖。移除顶部的_title字段后,自然不存在覆盖步骤,之前init()的赋值就能保留在实例上,行为也就符合预期了。
适配类字段书写习惯的解决方案
如果保持“类顶部集中声明所有字段”的书写习惯,不需要改动代码风格,只要调整初始化的触发时机即可,常见的两种适配方式:
- 调整父类的初始化逻辑:把
init()的调用从父类构造函数中移出,改在子类构造函数里、super()执行完成后手动调用,等类字段全部挂载完成后再执行初始化逻辑,即可避免覆盖问题 - 调整父类的延迟触发逻辑:如果不想每个子类都手动写
init()调用,可以在父类构造函数里将init()的执行放到当前调用栈走完之后(比如通过微任务队列延迟执行),等类字段的初始化流程全部完成后再触发init逻辑
内容的提问来源于stack exchange,提问作者Krzysztof Boronowski
相关产品推荐
相关产品推荐

