简体   繁体   English

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

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

I have a class A , and a class B inherited from it. 我有一个类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();

When I run this code, I get the following error: 运行此代码时,出现以下错误:

Uncaught TypeError: Cannot read property 'value' of undefined

How can I avoid this error? 如何避免此错误?

It's clear for me that the JavaScript code will call the init method before it creates the myMember , but there should be some practice/pattern to make it work. 对我来说很明显,JavaScript代码将在创建myMember之前调用init方法,但是应该有一些练习/模式来使其起作用。

This is why in some languages (cough C#) code analysis tools flag usage of virtual members inside constructors. 这就是为什么在某些语言(咳嗽C#)中,代码分析工具会标记构造函数内部虚拟成员的使用情况。

In Typescript field initializations happen in the constructor, after the call to the base constructor. 在Typescript字段中,初始化是在调用基本构造函数之后在构造函数中进行的。 The fact that field initializations are written near the field is just syntactic sugar. 字段初始化写在字段附近的事实只是语法糖。 If we look at the generated code the problem becomes clear: 如果我们查看生成的代码,问题将变得很清楚:

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;
}

You should consider a solution where init is either called from outside the instance, and not in the constructor: 您应该考虑从实例外部而不是在构造函数中调用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();   

Or you can have an extra parameter to your constructor that specifies whether to call init and not call it in the derived class as well. 或者,您可以在构造函数中使用一个额外的参数,该参数指定是否调用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();

Or the very very very dirty solution of setTimeout , which will defer initialization until the current frame completes. 或者setTimeout非常非常肮脏的解决方案,它将延迟初始化,直到当前帧完成为止。 This will let the parent constructor call to complete, but there will be an interim between constructor call and when the timeout expires when the object has not been init ed 这将使父构造函数调用完成,但是在构造函数调用与未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 

Because myMember property is accessed in parent constructor ( init() is called during super() call), there is no way how it can be defined in child constructor without hitting a race condition. 由于myMember属性是在父构造函数中访问的(在super()调用期间调用了init() ),因此无法在不违反竞争条件的情况下在子构造函数中对其进行定义。

There are several alternative approaches. 有几种替代方法。

init hook init

init is considered a hook that shouldn't be called in class constructor. init被认为是一个不应在类构造函数中调用的钩子。 Instead, it is called explicitly: 相反,它被显式调用:

new B();
B.init();

Or it is called implicitly by the framework, as a part of application lifecycle. 或者作为框架在应用程序生命周期中被框架隐式调用。

Static property 静态特性

If a property is supposed to be a constant, it can be static property. 如果属性应该是常量,则可以是静态属性。

This is the most efficient way because this is what static members are for, but the syntax may be not that attractive because it requires to use this.constructor instead of class name if static property should be properly referred in child classes: 这是最有效的方法,因为这是静态成员的用途,但是语法可能没有那么吸引人,因为如果在子类中正确引用静态属性,则需要使用this.constructor而不是类名:

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

    init() {
        console.log((this.constructor as typeof B).myMember.value);
    }
}

Property getter/setter 财产获取者/设定者

Property descriptor can be defined on class prototype with get / set syntax. 可以在类原型上使用get / set语法定义属性描述符。 If a property is supposed to be primitive constant, it can be just a getter: 如果一个属性应该是原始常量,那么它只能是一个吸气剂:

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

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

It becomes more hacky if the property is not constant or primitive: 如果该属性不是常量或原始的,它将变得更加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);
    }
}

In-place initialization 就地初始化

A property may be initialized where it's accessed first. 可以在首先访问属性的地方对其进行初始化。 If this happens in init method where this can be accessed prior to B class constructor, this should happen there: 如果发生这种情况在init方法,其中this之前可被访问B类的构造函数,这应该有发生:

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

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

Asynchronous initialization 异步初始化

init method may become asynchronous. init方法可能会变得异步。 Initialization state should be trackable, so the class should implement some API for that, eg promise-based: 初始化状态应该是可跟踪的,因此该类应该为此实现一些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
})

This approach may be considered antipattern for this particular case because initialization routine is intrinsically synchronous, but it may be suitable for asynchronous initialization routines. 对于这种特殊情况,此方法可以视为反模式,因为初始化例程本质上是同步的,但它可能适用于异步初始化例程。

Desugared class 脱糖级

Since ES6 classes have limitations on the use of this prior to super , child class can be desugared to a function to evade this limitation: 由于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);
}

This is rarely a good option, because desugared class should be additionally typed in TypeScript. 这很少是一个好的选择,因为应该在TypeScript中另外键入已终止的类。 This also won't work with native parent classes (TypeScript es6 and esnext target). 这也不适用于本机父类(TypeScript es6esnext目标)。

One approach you could take is use a getter/setter for myMember and manage the default value in the getter. 您可以采用的一种方法是使用myMember的getter / setter并管理getter中的默认值。 This would prevent the undefined problem and allow you to keep almost exactly the same structure you have. 这样可以防止出现未定义的问题,并使您保持几乎完全相同的结构。 Like this: 像这样:

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();

Try this: 尝试这个:

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();

Super has to be first command. 超级必须是第一个命令。 Remeber that typescript is more "javascript with documentation of types" rather than language on its own. 请记住,打字稿更像是“带有类型文档的javascript”,而不是单独的语言。

If you look to the transpiled code .js it is clearly visible: 如果查看转译的代码.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();

Do you have to call init in class A? 您必须在A类中调用init吗?

That works fine, but I don't know if you have different requirements: 那很好,但是我不知道您是否有不同的要求:

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();

Like this : 像这样 :

 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