我目前正在开发浏览器扩展来管理打开的选项卡,我注意到在 JS ES 中,当我在类的顶部声明类字段时,多态性有点奇怪。
假设我们想在对象初始化中使用多态性。
例如,我们有基类视图:
class View {
_viewModel;
constructor(viewModel) {
this._viewModel = viewModel;
this.init();
}
init() { }
}
和派生类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);
此示例的调用堆栈看起来正确(从上到下读取):
- 选项卡视图构造函数
- View contrustor(由 super() 调用) TabView
- init() (View 构造函数从 TabView 调用覆盖初始化方法)
选项卡视图的预期值:
- _viewModel:"模型">
- _title:"测试">
以下示例值为TabView:
- _viewModel:"模型">
- _title:"未定义">
当我调试此示例时,看起来当从View
调用init()
方法时,this
引用View
类而不是TabView
。该值保存在View
实例中,TabView
字段仍为"未定义"。当我从类的顶部删除_title
字段时TabView
一切都可以按我的意愿工作。最新版本的Firefox和Microsoft Edge的结果相同。
我喜欢在顶部写类字段,所以我想问一下这是 JS ES 的正确行为,还是在 ECMA 脚本的未来版本中可能会更正的错误?
当我调试此示例时,看起来当从
View
调用init()
方法时,this
引用View
类而不是TabView
。该值保存在View
实例中,并且TabView
字段仍'undefined'
。
看看这段代码:
class View {
_viewModel;
constructor(viewModel) {
this._viewModel = viewModel;
this.init();
}
init() { console.log("View init"); }
}
class TabView extends View {
_title;
constructor(viewModel) {
super(viewModel);
}
init() {
console.log("TabView init");
this.title = "test";
}
get title() {
console.log("get title");
return this._title;
}
set title(value) {
console.log("set title");
this._title = value;
}
}
const tabView = new TabView("model");
console.log(tabView.title);
此日志
TabView init
set title
get title
这意味着构造函数从TabView
调用init
,而又调用 setter 进行title
。
最终undefined
_title
的原因是类字段的规范(在撰写本文时,阶段 3 提案)。以下是相关部分:
没有初始值设定项的字段设置为
undefined
公共字段声明和私有字段声明都会在实例中创建一个字段,无论是否存在初始值设定项。如果没有初始值设定项,则该字段设置为
undefined
。这与某些转译器实现略有不同,后者只会完全忽略没有初始值设定项的字段声明。
因为_title
不是在TabView
内初始化的,规范定义它的值应该在构造函数完成执行后undefined
。
这里有几个选项,但如果你想将_title
声明为类字段并为其提供不同的值,则必须为该字段提供一个值作为TabView
实例化的一部分,而不是作为其父级(或祖父母等)的一部分。
字段初始化器
class TabView extends View {
_title = "test"; //give value to the field directly
constructor(viewModel) {
super(viewModel);
}
/* ... */
}
class View {
_viewModel;
constructor(viewModel) {
this._viewModel = viewModel;
this.init();
}
init() { }
}
class TabView extends View {
_title = "test"; //give value to the field directly
constructor(viewModel) {
super(viewModel);
}
get title() {
return this._title;
}
set title(value) {
this._title = value;
}
}
const tabView = new TabView("model");
console.log(tabView.title);
初始化构造函数中的值
class TabView extends View {
_title;
constructor(viewModel) {
super(viewModel);
this._title = "test"; //give value to `_title` in the constructor
}
/* ... */
}
class View {
_viewModel;
constructor(viewModel) {
this._viewModel = viewModel;
this.init();
}
init() { }
}
class TabView extends View {
_title;
constructor(viewModel) {
super(viewModel);
this._title = "test"; //give value in the constructor
}
get title() {
return this._title;
}
set title(value) {
this._title = value;
}
}
const tabView = new TabView("model");
console.log(tabView.title);
调用初始化字段的方法
class TabView extends View {
_title;
constructor(viewModel) {
super(viewModel);
this.init(); //call `init` which will give value to the `_title` field
}
init() {
this.title = "test";
}
/* ... */
}
class View {
_viewModel;
constructor(viewModel) {
this._viewModel = viewModel;
this.init();
}
init() { }
}
class TabView extends View {
_title;
constructor(viewModel) {
super(viewModel);
this.init(); //call `init` which will give value to the `_title` field
}
init() {
this.title = "test";
}
get title() {
return this._title;
}
set title(value) {
this._title = value;
}
}
const tabView = new TabView("model");
console.log(tabView.title);
删除字段声明
class TabView extends View {
//no declaration here
constructor(viewModel) {
super(viewModel);
}
/* ... */
}
class View {
_viewModel;
constructor(viewModel) {
this._viewModel = viewModel;
this.init();
}
init() { console.log("View init"); }
}
class TabView extends View {
//no declaration here
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);