繁体   English   中英

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

[英]Typescript: can not access member value in inherited class constructor

我有一个类A和一个从它继承的类B

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代码将在创建myMember之前调用init方法,但是应该有一些练习/模式来使其起作用。

这就是为什么在某些语言(咳嗽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属性是在父构造函数中访问的(在super()调用期间调用了init() ),因此无法在不违反竞争条件的情况下在子构造函数中对其进行定义。

有几种替代方法。

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语法定义属性描述符。 如果一个属性应该是原始常量,那么它只能是一个吸气剂:

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,例如,基于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类在使用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 es6esnext目标)。

您可以采用的一种方法是使用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();

超级必须是第一个命令。 请记住,打字稿更像是“带有类型文档的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();

您必须在A类中调用init吗?

那很好,但是我不知道您是否有不同的要求:

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

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

const x = new B();

像这样 :

 class A
{
     myMember; 
    constructor() {

    }

    show() {
        alert(this.myMember.value);
    }
}

class B extends A {
    public myMember = {value:1};

    constructor() {
        super();
    }
}

const test = new B;
test.show();

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM