web-dev-qa-db-ja.com

Typescript:継承されたクラスコンストラクターのメンバー値にアクセスできません

クラス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コードがinitを作成する前にmyMemberメソッドを呼び出すことは私には明らかですが、それを機能させるには、いくつかのプラクティス/パターンが必要です。

19
Adam

これが、一部の言語(咳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の非常に非常にダーティなソリューション。これにより、親コンストラクターの呼び出しは完了しますが、コンストラクターの呼び出しと、オブジェクトがinitedされていないときにタイムアウトになるまでの間に暫定的になります。

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

プロパティゲッター/セッター

プロパティ記述子は、get/set構文を使用してクラスプロトタイプで定義できます。プロパティがプリミティブ定数であると想定されている場合、それは単なるゲッターである可能性があります。

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メソッドで発生する場合、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クラスは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でさらに入力する必要があるため、これはめったに良いオプションではありません。これは、ネイティブの親クラス(TypeScript es6およびesnextターゲット)でも機能しません。

5
Estus Flask

あなたが取ることができる1つのアプローチは、myMemberにゲッター/セッターを使用し、ゲッターでデフォルト値を管理することです。これにより、未定義の問題が回避され、ほぼ同じ構造を維持できます。このような:

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

スーパーは最初のコマンドでなければなりません。 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();
2
libik

クラス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 {
    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();
2
Igor Dimchevski

このような :

 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();
0
Léo R.