问题描述
我目前正在开发一个浏览器扩展来管理打开的选项卡,我注意到在 JS ES 中,当我在类的顶部声明类字段时,多态性的工作有点奇怪。
假设我们想在对象初始化中使用多态。
例如我们有基类 View:
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);
此示例的调用堆栈看起来正确(从上到下读取):
TabView 的预期值:
- _viewmodel:“模型”
- _title: "测试"
TabView 的示例值:
- _viewmodel:“模型”
- _title: "未定义"
当我调试这个例子时,看起来当从 init()
调用 View
方法时,this
指的是 View
类而不是 TabView
。该值已保存在 View
实例中,而 TabView
字段仍为“未定义”。当我从 _title
类的顶部删除 TabView
字段时,一切都如我所愿。最新版本的 Firefox 和 Microsoft Edge 的结果相同。
我喜欢将类字段写在顶部,所以我想问一下它是否是 JS ES 的正确行为,或者它是否是一个可能会在未来版本的 ECMA 脚本中更正的错误?
解决方法
当我调试这个例子时,看起来当从 init()
调用 View
方法时,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
这意味着构造函数从 init
调用 TabView
,后者又调用 title
的 setter。
_title
最终是 undefined
的原因是 the specification for class fields(在撰写本文时为第 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);