typescript:无法访问继承类构造函数中的成员值

IT技术 javascript typescript oop inheritance ecmascript-6
2021-03-06 13:55:03

我有一个 class A,还有一个B继承自它的 class

class A {
    constructor(){
        this.init();
    }
    init(){}
}

class B extends A {
    private myMember = {value:1};
    constructor(){
        super();
    }
    init(){
        console.log(this.myMember.value);
    }
}

const x = new B();

当我运行此代码时,出现以下错误:

Uncaught TypeError: Cannot read property 'value' of undefined

我怎样才能避免这个错误?

我很清楚 JavaScript 代码将init在创建 之前调用该方法myMember,但应该有一些实践/模式才能使其工作。

6个回答

这就是为什么在某些语言(咳嗽 C#)中,代码分析工具会标记构造函数中虚拟成员的使用情况。

在 Typescript 中,字段初始化发生在构造函数中,在调用基本构造函数之后。字段初始化写在字段附近的事实只是语法糖。如果我们查看生成的代码,问题就很清楚了:

function B() {
    var _this = _super.call(this) || this; // base call here, field has not been set, init will be called
    _this.myMember = { value: 1 }; // field init here
    return _this;
}

您应该考虑一个解决方案,其中 init 从实例外部调用,而不是在构造函数中调用:

class A {
    constructor(){
    }
    init(){}
}

class B extends A {
    private myMember = {value:1};
    constructor(){
        super();
    }
    init(){
        console.log(this.myMember.value);
    }
}

const x = new B();
x.init();   

或者,您可以为构造函数添加一个额外的参数,用于指定是否init在派生类中调用和不调用它。

class A {
    constructor()
    constructor(doInit: boolean)
    constructor(doInit?: boolean){
        if(doInit || true)this.init();
    }
    init(){}
}

class B extends A {
    private myMember = {value:1};
    constructor()
    constructor(doInit: boolean)
    constructor(doInit?: boolean){
        super(false);
        if(doInit || true)this.init();
    }
    init(){
        console.log(this.myMember.value);
    }
}

const x = new B();

或者非常非常非常脏的解决方案setTimeout,它将延迟初始化,直到当前帧完成。这将让父构造函数调用完成,但在构造函数调用和超时到期时对象尚未被init编辑之间会有一个过渡期

class A {
    constructor(){
        setTimeout(()=> this.init(), 1);
    }
    init(){}
}

class B extends A {
    private myMember = {value:1};
    constructor(){
        super();
    }
    init(){
        console.log(this.myMember.value);
    }
}

const x = new B();
// x is not yet inited ! but will be soon 

因为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);
    }
}

属性 getter/setter

可以使用get/set语法在类原型上定义属性描述符如果一个属性应该是原始常量,它可以只是一个 getter:

class B extends A {
    get myMember() {
        return 1;
    }

    init() {
        console.log(this.myMember);
    }
}

如果属性不是恒定的或原始的,它会变得更加棘手:

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方法中,这应该发生在那里:thisB

class B extends A {
    private myMember?: { value: number };

    init() {
        this.myMember = { value: 1 }; 
        console.log(this.myMember.value);
    }
}

异步初始化

init方法可能会变得异步。初始化状态应该是可跟踪的,因此该类应该为此实现一些 API,例如基于 promise:

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类对使用限制this之前super,子类可以被脱到一个函数来规避这一限制:

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 中额外输入脱糖类。这也不适用于本机父类(TypeScriptes6esnext目标)。

您可以采用的一种方法是为myMember使用 getter/setter并管理 getter 中的默认值。这将防止未定义的问题,并允许您保持几乎完全相同的结构。像这样:

class A {
    constructor(){
        this.init();
    }
    init(){}
}

class B extends A {
    private _myMember;
    constructor(){
        super();
    }
    init(){
        console.log(this.myMember.value);
    }

    get myMember() {
        return this._myMember || { value: 1 };
    }

    set myMember(val) {
        this._myMember = val;
    }
}

const x = new B();

试试这个:

class A {
    constructor() {
        this.init();
    }
    init() { }
}

class B extends A {
    private myMember = { 'value': 1 };
    constructor() {
        super();
    }
    init() {
        this.myMember = { 'value': 1 };
        console.log(this.myMember.value);
    }
}

const x = new B();
是的,但在这种情况下,我必须重新声明myMember我不想这样做。
2021-04-27 13:55:03

超级必须是第一个命令。请记住,typescript更像是“带有类型文档的 javascript”,而不是其自身的语言。

如果您查看转换后的代码 .js,它会清晰可见:

class A {
    constructor() {
        this.init();
    }
    init() {
    }
}
class B extends A {
    constructor() {
        super();
        this.myMember = { value: 1 };
    }
    init() {
        console.log(this.myMember.value);
    }
}
const x = new B();
@Adam - 没有解决方案,你不能像这样使用它
2021-04-23 13:55:03
好的,就是这样,对不起。但这不是我问题的解决方案:-/
2021-04-30 13:55:03
它的作用相同。init()将在this.myMember声明之前被调用
2021-05-08 13:55:03
@Adam - 再次阅读答案,我已经发布了由typescript创建的 .js 文件。
2021-05-13 13:55:03