多态性在 JS ES 中不是错误的吗

Isn't polymorphism works wrong in JS ES

我目前正在开发一个浏览器扩展来管理打开的选项卡,我注意到在 JS ES 中,当我在 class 顶部声明 class 字段时,多态性工作起来有点奇怪。

假设我们想在对象初始化中使用多态性。

例如我们有基础 class View:

class View {
    _viewModel;

    constructor(viewModel) {
        this._viewModel = viewModel;
        this.init();
    }

    init() { }
}

并导出classTabView:

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 的预期值

此示例的 TabView 值

当我调试这个例子时,看起来当从 View 调用 init() 方法时 this 指的是 View class 而不是 TabView。该值保存在 View 实例中,并且 TabView 字段仍然是 'undefined'。当我从 TabView class 的顶部删除 _title 字段时,一切都按我的意愿进行。最新版本的 Firefox 和 Microsoft Edge 的结果相同。

我喜欢将 class 字段写在顶部,所以我想问一下它是否是 JS ES 的正确行为,或者它可能是一个错误,可能会在未来的 ECMA 脚本版本中更正?

When I debug this example, it looks like when init() method is invoked from View then this refers to View class instead of TabView. The value was saved in View instance, and TabView field was still '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,后者又为 title.

调用 setter

_titleundefined 的原因最终是 the specification for class fields(在撰写本文时是第 3 阶段提案)。这是相关部分:

Fields without initializers are set to undefined

Both public and private field declarations create a field in the instance, whether or not there's an initializer present. If there's no initializer, the field is set to undefined. This differs a bit from certain transpiler implementations, which would just entirely ignore a field declaration which has no initializer.

因为_title没有在TabView内初始化,规范定义在构造器执行完之后它的值应该是undefined

这里有几个选项,但是如果你想将 _title 声明为 class 字段, 有不同的值,你有给字段一个值作为 TabView 实例化的一部分,而不是作为其 parent(或 grandparents 等)的一部分。

字段初始化程序

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);