因为myMember 属性是在父构造函数中访问的(init() 在super() 调用期间被调用),所以没有办法在子构造函数中定义它而不遇到竞争条件。
有几种替代方法。
init钩子
init 被认为是一个不应在类构造函数中调用的钩子。相反,它被显式调用:
new B();
B.init();
或者它被框架隐式调用,作为应用程序生命周期的一部分。
静态属性
如果一个属性应该是一个常量,它可以是静态属性。
这是最有效的方法,因为这是静态成员的用途,但语法可能没有那么吸引人,因为如果应在子类中正确引用静态属性,则需要使用 this.constructor 而不是类名:
class B extends A {
static readonly myMember = { value: 1 };
init() {
console.log((this.constructor as typeof B).myMember.value);
}
}
属性获取器/设置器
可以使用get/set 语法在类原型上定义属性描述符。如果一个属性应该是原始常量,它可以只是一个 getter:
class B extends A {
get myMember() {
return 1;
}
init() {
console.log(this.myMember);
}
}
如果属性不是常量或原始的,它会变得更加hacky:
class B extends A {
private _myMember?: { value: number };
get myMember() {
if (!('_myMember' in this)) {
this._myMember = { value: 1 };
}
return this._myMember!;
}
set myMember(v) {
this._myMember = v;
}
init() {
console.log(this.myMember.value);
}
}
就地初始化
一个属性可以在它首先被访问的地方被初始化。如果这种情况发生在 init 方法中,其中 this 可以在 B 类构造函数之前访问,这应该发生在那里:
class B extends A {
private myMember?: { value: number };
init() {
this.myMember = { value: 1 };
console.log(this.myMember.value);
}
}
异步初始化
init 方法可能会变成异步的。初始化状态应该是可跟踪的,因此该类应该为此实现一些 API,例如基于承诺:
class A {
initialization = Promise.resolve();
constructor(){
this.init();
}
init(){}
}
class B extends A {
private myMember = {value:1};
init(){
this.initialization = this.initialization.then(() => {
console.log(this.myMember.value);
});
}
}
const x = new B();
x.initialization.then(() => {
// class is initialized
})
对于这种特殊情况,这种方法可能被视为反模式,因为初始化例程本质上是同步的,但它可能适用于异步初始化例程。
脱糖类
由于 ES6 类在 super 之前对 this 的使用有限制,因此可以将子类脱糖为函数来规避此限制:
interface B extends A {}
interface BPrivate extends B {
myMember: { value: number };
}
interface BStatic extends A {
new(): B;
}
const B = <BStatic><Function>function B(this: BPrivate) {
this.myMember = { value: 1 };
return A.call(this);
}
B.prototype.init = function () {
console.log(this.myMember.value);
}
这很少是一个好的选择,因为脱糖类应该在 TypeScript 中额外键入。这也不适用于原生父类(TypeScript es6 和 esnext 目标)。