25

我写了一些代码:

class Base {
    // Default value
    myColor = 'blue';

    constructor() {
        console.log(this.myColor);
    }
}

class Derived extends Base {
     myColor = 'red'; 
}

// Prints "blue", expected "red"
const x = new Derived();

我期待我的派生类字段初始化程序在基类构造函数之前运行。myColor相反,派生类直到基类构造函数运行后才会更改属性,因此我观察到构造函数中的错误值。

这是一个错误吗?怎么了?为什么会这样?我应该怎么做?

4

2 回答 2

39

不是错误

首先,这不是 TypeScript、Babel 或 JS 运行时中的错误。

为什么必须这样

您的第一个跟进可能是“为什么不正确执行此操作!?!?”。让我们来看看 TypeScript emit 的具体情况。实际答案取决于我们为哪个版本的 ECMAScript 发出类代码。

下层发射:ES3/ES5

让我们检查一下 TypeScript 为 ES3 或 ES5 发出的代码。为了便于阅读,我对此进行了简化+注释:

var Base = (function () {
    function Base() {
        // BASE CLASS PROPERTY INITIALIZERS
        this.myColor = 'blue';
        console.log(this.myColor);
    }
    return Base;
}());

var Derived = (function (_super) {
    __extends(Derived, _super);
    function Derived() {
        // RUN THE BASE CLASS CTOR
        _super();

        // DERIVED CLASS PROPERTY INITIALIZERS
        this.myColor = 'red';

        // Code in the derived class ctor body would appear here
    }
    return Derived;
}(Base));

基类 emit 毫无争议是正确的 - 字段被初始化,然后构造函数主体运行。您当然不想要相反的情况 -在运行构造函数主体之前初始化字段意味着您在构造函数之后才能看到字段值,这不是任何人想要的。

派生类发出正确吗?

不,你应该交换订单

很多人会争辩说派生类 emit 应该是这样的:

    // DERIVED CLASS PROPERTY INITIALIZERS
    this.myColor = 'red';

    // RUN THE BASE CLASS CTOR
    _super();

由于多种原因,这是非常错误的:

  • 它在 ES6 中没有相应的行为(参见下一节)
  • 的值'red'myColor立即被基类值“蓝色”覆盖
  • 派生类字段初始化器可能会调用依赖于基类初始化的基类方法。

关于最后一点,请考虑以下代码:

class Base {
    thing = 'ok';
    getThing() { return this.thing; }
}
class Derived extends Base {
    something = this.getThing();
}

如果派生类初始化程序在基类初始化程序之前运行,则Derived#something始终是undefined,而显然应该是'ok'

不,你应该使用时光机

许多其他人会争辩说应该做一个模糊的其他事情,以便知道BaseDerived一个字段初始值设定项。

您可以编写依赖于了解要运行的整个代码领域的示例解决方案。但是 TypeScript / Babel / etc 不能保证这个存在。例如,Base可以在一个单独的文件中,我们看不到它的实现。

下层发射:ES6

如果您还不知道这一点,那么是时候学习了:类不是 TypeScript 的特性。它们是 ES6 的一部分并且已经定义了语义。但是 ES6 类不支持字段初始值设定项,因此它们被转换为与 ES6 兼容的代码。它看起来像这样:

class Base {
    constructor() {
        // Default value
        this.myColor = 'blue';
        console.log(this.myColor);
    }
}
class Derived extends Base {
    constructor() {
        super(...arguments);
        this.myColor = 'red';
    }
}

代替

    super(...arguments);
    this.myColor = 'red';

我们应该有这个吗?

    this.myColor = 'red';
    super(...arguments);

不,因为它不起作用。在派生类中this调用之前引用是非法的。super它根本无法以这种方式工作。

ES7+:公共字段

控制 JavaScript 的 TC39 委员会正在研究将字段初始化器添加到该语言的未来版本中。

您可以在 GitHub 上阅读它阅读有关初始化顺序的特定问题

OOP 复习:构造函数的虚拟行为

所有 OOP 语言都有一个通用指南,有些是明确强制执行的,有些是按照约定隐式执行的:

不要从构造函数调用虚方法

例子:

在 JavaScript 中,我们必须将这条规则扩展一点

不要观察构造函数的虚拟行为

类属性初始化算作虚拟

解决方案

标准解决方案是将字段初始化转换为构造函数参数:

class Base {
    myColor: string;
    constructor(color: string = "blue") {
        this.myColor = color;
        console.log(this.myColor);
    }
}

class Derived extends Base {
    constructor() {
        super("red");
     }
}

// Prints "red" as expected
const x = new Derived();

您也可以使用init模式,但需要注意不要从中观察虚拟行为,并且不要在派生init方法中执行需要完全初始化基类的操作:

class Base {
    myColor: string;
    constructor() {
        this.init();
        console.log(this.myColor);
    }
    init() {
        this.myColor = "blue";
    }
}

class Derived extends Base {
    init() {
        super.init();
        this.myColor = "red";
    }
}

// Prints "red" as expected
const x = new Derived();
于 2017-04-24T19:08:31.357 回答
5

我会恭敬地争辩说这实际上是一个错误

通过做意想不到的事情,这是破坏常见类扩展用例的不良行为。这是支持您的用例的初始化顺序,我认为它更好:

Base property initializers
Derived property initializers
Base constructor
Derived constructor

问题/解决方案

- typescript 编译器当前在构造函数中发出属性初始化

这里的解决方案是将属性初始化与构造函数的调用分开。C# 会这样做,尽管它在派生属性之后初始化基础属性,这也是违反直觉的。这可以通过发出辅助类来完成,以便派生类可以以任意顺序初始化基类。

class _Base {
    ctor() {
        console.log('base ctor color: ', this.myColor);
    }

    initProps() {
        this.myColor = 'blue';
    }
}
class _Derived extends _Base {
    constructor() {
        super();
    }

    ctor() {
        super.ctor();
        console.log('derived ctor color: ', this.myColor);
    }

    initProps() {
        super.initProps();
        this.myColor = 'red';
    }
}

class Base {
    constructor() {
        const _class = new _Base();
        _class.initProps();
        _class.ctor();
        return _class;
    }
}
class Derived {
    constructor() {
        const _class = new _Derived();
        _class.initProps();
        _class.ctor();
        return _class;
    }
}

// Prints:
// "base ctor color: red"
// "derived ctor color: red"
const d = new Derived();

- 基础构造函数不会因为我们使用派生类属性而中断吗?

任何在基本构造函数中中断的逻辑都可以移动到派生类中将被覆盖的方法。由于派生方法是在调用基本构造函数之前初始化的,因此可以正常工作。例子:

class Base {
    protected numThings = 5;

    constructor() {
        console.log('math result: ', this.doMath())
    }

    protected doMath() {
        return 10/this.numThings;
    }
}

class Derived extends Base {
    // Overrides. Would cause divide by 0 in base if we weren't overriding doMath
    protected numThings = 0;

    protected doMath() {
        return 100 + this.numThings;
    }
}

// Should print "math result: 100"
const x = new Derived();
于 2018-10-02T06:29:25.200 回答