web-dev-qa-db-ja.com

派生クラスのプロパティ値が基本クラスコンストラクターに表示されないのはなぜですか?

私はいくつかのコードを書きました:

class Base {
    // Default value
    myColor = 'blue';

    constructor() {
        console.log(this.myColor);
    }
}

class Derived extends Base {
     myColor = 'red'; 
}

// Prints "blue", expected "red"
const x = new Derived();

派生クラスフィールドの初期化子が、基本クラスコンストラクターの前に実行されることを期待していました。代わりに、派生クラスは、基本クラスコンストラクターが実行されるまでmyColorプロパティを変更しないため、コンストラクターの誤った値を確認します。

これはバグですか?どうしましたか?なぜこれが起こるのですか?代わりに何をすべきですか?

22
Ryan Cavanaugh

バグではない

まず、これはTypeScript、Babel、またはJSランタイムのバグではありません。

なぜそれがこのようでなければならないのか

あなたが持っているかもしれない最初のフォローアップは「なぜこれをしないのか正しく!?!?」です。 TypeScriptエミットの特定のケースを調べてみましょう。実際の答えは、クラスコードを出力するECMAScriptのバージョンによって異なります。

ダウンレベル放出:ES3/ES5

ES3またはES5のTypeScriptによって生成されたコードを調べてみましょう。読みやすくするために、これに少し+注釈を付けました。

var Base = (function () {
    function Base() {
        // BASE CLASS PROPERTY INITIALIZERS
        this.myColor = 'blue';
        console.log(this.myColor);
    }
    return Base;
}());

var Derived = (function (_super) {
    __extends(Derived, _super);
    function Derived() {
        // RUN THE BASE CLASS CTOR
        _super();

        // DERIVED CLASS PROPERTY INITIALIZERS
        this.myColor = 'red';

        // Code in the derived class ctor body would appear here
    }
    return Derived;
}(Base));

基本クラスの出力は議論の余地なく正しいです-フィールドが初期化され、その後コンストラクター本体が実行されます。あなたは確かに反対を望んでいないでしょう-フィールドの初期化beforeコンストラクタ本体を実行することはafterまでフィールド値を見ることができなかったことを意味しますが、これは何ですか誰もが望んでいる。

派生クラスは正しいエミットですか?

いいえ、注文を入れ替える必要があります

多くの人は、派生クラスの出力は次のようになるはずだと主張します。

    // DERIVED CLASS PROPERTY INITIALIZERS
    this.myColor = 'red';

    // RUN THE BASE CLASS CTOR
    _super();

これはいくつかの理由で非常に間違っています:

  • ES6では対応する動作はありません(次のセクションを参照)
  • 'red' for myColorは、基本クラス値 'blue'によってすぐに上書きされます
  • 派生クラスフィールドの初期化子は、基本クラスの初期化に依存する基本クラスメソッドを呼び出す場合があります。

最後の点で、次のコードを検討してください。

class Base {
    thing = 'ok';
    getThing() { return this.thing; }
}
class Derived extends Base {
    something = this.getThing();
}

派生クラス初期化子が基本クラス初期化子の前に実行された場合、Derived#somethingは常にundefinedですが、明らかに'ok'

いいえ、タイムマシンを使用する必要があります

他の多くの人々は、BaseDerivedにフィールド初期化子があることを認識できるように、曖昧な何か他のを実行する必要があると主張します。

実行するコード全体を知ることに依存するソリューションの例を書くことができます。しかし、TypeScript/Babel /などは、これが存在することを保証できません。たとえば、Baseは、その実装を確認できない別のファイルに置くことができます。

ダウンレベル放出:ES6

これをまだご存じない場合は、学習する時間です。クラスはTypeScript機能ではありません。これらはES6の一部であり、セマンティクスが定義されています。ただし、ES6クラスはフィールド初期化子をサポートしていないため、ES6互換コードに変換されます。次のようになります。

class Base {
    constructor() {
        // Default value
        this.myColor = 'blue';
        console.log(this.myColor);
    }
}
class Derived extends Base {
    constructor() {
        super(...arguments);
        this.myColor = 'red';
    }
}

の代わりに

    super(...arguments);
    this.myColor = 'red';

これ持っておくべき?

    this.myColor = 'red';
    super(...arguments);

いいえ、機能しないため。派生クラスでthisを呼び出す前にsuperを参照することはできません。それは単にこのように機能することはできません。

ES7 +:パブリックフィールド

JavaScriptを管理するTC39委員会は、フィールド初期化子を将来のバージョンの言語に追加することを調査しています。

GitHubで読んでください または 初期化の順序に関する特定の問題を読んでください

OOP復習:コンストラクターからの仮想動作

すべてのOOP言語には一般的なガイドラインがあり、一部は明示的に実施され、一部は慣例により暗黙的に実施されます。

コンストラクターから仮想メソッドを呼び出さない

例:

JavaScriptでは、このルールを少し拡張する必要があります

コンストラクターから仮想動作を観察しない

そして

クラスプロパティの初期化は仮想としてカウントされます

ソリューション

標準的な解決策は、フィールドの初期化をコンストラクターパラメーターに変換することです。

class Base {
    myColor: string;
    constructor(color: string = "blue") {
        this.myColor = color;
        console.log(this.myColor);
    }
}

class Derived extends Base {
    constructor() {
        super("red");
     }
}

// Prints "red" as expected
const x = new Derived();

initパターンを使用することもできますが、注意してnotそこから仮想動作を観察するand派生したinitメソッドで完全な初期化を必要とすることを行わない基本クラスの:

class Base {
    myColor: string;
    constructor() {
        this.init();
        console.log(this.myColor);
    }
    init() {
        this.myColor = "blue";
    }
}

class Derived extends Base {
    init() {
        super.init();
        this.myColor = "red";
    }
}

// Prints "red" as expected
const x = new Derived();
28
Ryan Cavanaugh