不是错误
首先,这不是 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'
。
不,你应该使用时光机
许多其他人会争辩说应该做一个模糊的其他事情,以便知道Base
有Derived
一个字段初始值设定项。
您可以编写依赖于了解要运行的整个代码领域的示例解决方案。但是 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();