クラスの継承は、あるクラスが別のクラスを拡張する方法です。
したがって、既存の機能の上に新しい機能を作成できます。
Animal
クラスがあるとします。
クラス動物{ コンストラクター(名前) { this.speed = 0; this.name = 名前; } 実行(速度) { this.speed = 速度; alert(`${this.name} は ${this.speed} の速度で実行されます。`); } 停止() { this.speed = 0; alert(`${this.name} は静止しています。`); } } let Animal = new Animal("私の動物");
animal
オブジェクトとAnimal
クラスをグラフィカルに表現する方法は次のとおりです。
…そして、別のclass Rabbit
を作成したいと思います。
ウサギは動物であるため、「一般的な」動物ができることをウサギができるように、 Rabbit
クラスはAnimal
に基づいており、動物のメソッドにアクセスできる必要があります。
別のクラスを拡張する構文は、 class Child extends Parent
です。
Animal
を継承するclass Rabbit
を作成しましょう。
クラス Rabbit extends Animal { 隠れる() { alert(`${this.name} は非表示になります!`); } } let Rabbit = new Rabbit("White Rabbit"); ウサギ.run(5); // ホワイトラビットは速度 5 で実行します。 うさぎ.隠す(); // 白うさぎが隠れています!
Rabbit
クラスのオブジェクトは、 rabbit.hide()
などのRabbit
メソッドと、 rabbit.run()
などのAnimal
メソッドの両方にアクセスできます。
内部的には、古き良きプロトタイプの仕組みを使用してキーワードの動作extends
。 Rabbit.prototype.[[Prototype]]
Animal.prototype
に設定します。したがって、メソッドがRabbit.prototype
に見つからない場合、JavaScript はAnimal.prototype
からメソッドを取得します。
たとえば、 rabbit.run
メソッドを見つけるために、エンジンは以下をチェックします (図の下から上)。
rabbit
オブジェクト ( run
はありません)。
そのプロトタイプ、つまりRabbit.prototype
( hide
はありますが、 run
ありません)。
そのプロトタイプ、つまり ( extends
による) Animal.prototype
には、最終的にrun
メソッドが含まれます。
「ネイティブ プロトタイプ」の章からわかるように、JavaScript 自体は組み込みオブジェクトのプロトタイプ継承を使用します。たとえば、 Date.prototype.[[Prototype]]
はObject.prototype
です。これが、日付が汎用オブジェクト メソッドにアクセスできる理由です。
extends
の後には任意の式を使用できます
クラス構文では、クラスだけでなく、 extends
の後に任意の式を指定できます。
たとえば、親クラスを生成する関数呼び出しは次のようになります。
関数 f(フレーズ) { 戻りクラス { SayHi() { アラート(フレーズ); } }; } class User extends f("Hello") {} new User().sayHi(); // こんにちは
ここで、 class User
f("Hello")
の結果を継承します。
これは、関数を使用してさまざまな条件に応じてクラスを生成し、そこから継承できる高度なプログラミング パターンに役立つ場合があります。
次に、先に進んでメソッドをオーバーライドしましょう。デフォルトでは、 class Rabbit
で指定されていないすべてのメソッドはclass Animal
から「そのまま」直接取得されます。
ただし、 Rabbit
でstop()
などの独自のメソッドを指定すると、代わりにそれが使用されます。
クラス Rabbit extends Animal { 停止() { // ...これで、rabbit.stop() に使用されます。 // クラス Animal の stop() の代わりに } }
ただし、通常は親メソッドを完全に置き換えることは望ましくなく、むしろその上に構築してその機能を微調整したり拡張したりする必要があります。メソッド内で何かを行いますが、その前後またはプロセス内で親メソッドを呼び出します。
クラスはそのための"super"
キーワードを提供します。
super.method(...)
親メソッドを呼び出します。
super(...)
は親コンストラクターを呼び出します (コンストラクター内のみ)。
たとえば、ウサギが停止したときに自動的に隠れるようにします。
クラス動物{ コンストラクター(名前) { this.speed = 0; this.name = 名前; } 実行(速度) { this.speed = 速度; alert(`${this.name} は ${this.speed} の速度で実行されます。`); } 停止() { this.speed = 0; alert(`${this.name} は静止しています。`); } } クラス Rabbit extends Animal { 隠れる() { alert(`${this.name} は非表示になります!`); } 停止() { super.stop(); // 親の stop を呼び出す this.hide(); // そして非表示にします } } let Rabbit = new Rabbit("White Rabbit"); ウサギ.run(5); // ホワイトラビットは速度 5 で実行します。 うさぎ.stop(); // 白ウサギは静止しています。白ウサギが隠れてる!
これで、 Rabbit
プロセス内で親super.stop()
を呼び出すstop
メソッドが追加されました。
アロー関数にはsuper
がありません
「アロー関数の再考」の章で述べたように、アロー関数にはsuper
がありません。
アクセスされた場合は、外部関数から取得されます。例えば:
クラス Rabbit extends Animal { 停止() { setTimeout(() => super.stop(), 1000); // 1秒後に親を呼び出す stop } }
アロー関数のsuper
stop()
のスーパーと同じであるため、意図したとおりに機能します。ここで「通常の」関数を指定すると、エラーが発生します。
// 予想外のスーパー setTimeout(function() { super.stop() }, 1000);
コンストラクターを使用する場合、少し注意が必要になります。
これまで、 Rabbit
独自のconstructor
がありませんでした。
仕様によれば、クラスが別のクラスを拡張し、 constructor
を持たない場合、次の「空の」 constructor
が生成されます。
クラス Rabbit extends Animal { // 独自のコンストラクターを持たずにクラスを拡張するために生成される コンストラクター(...args) { super(...args); } }
ご覧のとおり、基本的には親constructor
を呼び出し、すべての引数を渡します。これは、独自のコンストラクターを作成しない場合に起こります。
次に、カスタム コンストラクターをRabbit
に追加しましょう。 name
に加えてearLength
指定します。
クラス動物{ コンストラクター(名前) { this.speed = 0; this.name = 名前; } // ... } クラス Rabbit extends Animal { コンストラクター(名前、earLength) { this.speed = 0; this.name = 名前; this.earLength = 耳の長さ; } // ... } // 機能しません! let Rabbit = new Rabbit("White Rabbit", 10); // エラー: これは定義されていません。
おっと!エラーが発生しました。今ではウサギを作ることはできません。何が間違っていたのでしょうか?
短い答えは次のとおりです。
継承クラスのコンストラクターはsuper(...)
を呼び出す必要があり、 (!) this
使用する前にそれを実行する必要があります。
…しかし、なぜ?ここで何が起こっているのでしょうか?確かに、この要件は奇妙に思えます。
もちろん説明も付いています。何が起こっているのかを本当に理解できるように、詳細を見てみましょう。
JavaScript では、継承クラスのコンストラクター関数 (いわゆる「派生コンストラクター」) と他の関数との間に区別があります。派生コンストラクターには特別な内部プロパティ[[ConstructorKind]]:"derived"
があります。それは特別な内部ラベルです。
そのラベルはnew
での動作に影響を与えます。
通常の関数がnew
で実行されると、空のオブジェクトが作成され、 this
に割り当てられます。
ただし、派生コンストラクターが実行される場合、これは行われません。親コンストラクターがこのジョブを実行することが期待されます。
したがって、派生コンストラクターは、親 (ベース) コンストラクターを実行するためにsuper
を呼び出す必要があります。そうしないと、 this
コンストラクターのオブジェクトは作成されません。そしてエラーが発生します。
Rabbit
コンストラクターが機能するには、次のようにthis
使用する前にsuper()
を呼び出す必要があります。
クラス動物{ コンストラクター(名前) { this.speed = 0; this.name = 名前; } // ... } クラス Rabbit extends Animal { コンストラクター(名前、earLength) { スーパー(名前); this.earLength = 耳の長さ; } // ... } // これで大丈夫です let Rabbit = new Rabbit("White Rabbit", 10); アラート(ウサギの名前); // 白うさぎ アラート(rabbit.earLength); // 10
高度なメモ
このメモは、おそらく他のプログラミング言語でのクラスに関する一定の経験があることを前提としています。
これは言語についてのより良い洞察を提供し、バグの原因となる可能性のある動作についても説明します (ただし、それほど頻繁ではありません)。
理解するのが難しいと感じた場合は、そのまま読み続けて、しばらくしてから戻ってください。
メソッドだけでなくクラスフィールドもオーバーライドできます。
ただし、親コンストラクターでオーバーライドされたフィールドにアクセスするときは、他のほとんどのプログラミング言語とはまったく異なる、注意が必要な動作があります。
次の例を考えてみましょう。
クラス動物{ 名前 = '動物'; コンストラクター() { アラート(この名前); // (*) } } クラス Rabbit extends Animal { 名前 = 'ウサギ'; } 新しい動物(); // 動物 新しいウサギ(); // 動物
ここで、 Rabbit
クラスはAnimal
を拡張し、 name
フィールドを独自の値でオーバーライドします。
Rabbit
には独自のコンストラクターがないため、 Animal
コンストラクターが呼び出されます。
興味深いのは、 new Animal()
とnew Rabbit()
のどちらの場合でも、行(*)
のalert
にはanimal
表示されていることです。
つまり、親コンストラクターは、オーバーライドされたフィールド値ではなく、常に独自のフィールド値を使用します。
何がおかしいのでしょうか?
まだ明確でない場合は、方法と比較してください。
以下は同じコードですが、 this.name
フィールドの代わりにthis.showName()
メソッドを呼び出します。
クラス動物{ showName() { // this.name の代わりに = 'animal' アラート('動物'); } コンストラクター() { this.showName(); //alert(this.name) の代わりに; } } クラス Rabbit extends Animal { showName() { アラート('ウサギ'); } } 新しい動物(); // 動物 新しいウサギ(); // うさぎ
注意してください: 現在の出力は異なります。
そしてそれは私たちが自然に期待していることです。親コンストラクターが派生クラスで呼び出される場合、オーバーライドされたメソッドが使用されます。
…しかし、クラスフィールドの場合はそうではありません。前述のとおり、親コンストラクターは常に親フィールドを使用します。
なぜ違いがあるのでしょうか?
その理由は、フィールドの初期化順序にあります。クラスフィールドが初期化されます。
基本クラスのコンストラクター (何も拡張しない) の前に、
派生クラスのsuper()
直後。
この例では、 Rabbit
派生クラスです。そこにはconstructor()
ありません。前に述べたように、これはsuper(...args)
のみを持つ空のコンストラクターがある場合と同じです。
したがって、 new Rabbit()
super()
を呼び出して親コンストラクターを実行し、(派生クラスのルールに従って) その後にのみそのクラス フィールドが初期化されます。親コンストラクターの実行時には、まだRabbit
クラスのフィールドが存在しないため、 Animal
フィールドが使用されます。
フィールドとメソッド間のこの微妙な違いは、JavaScript に特有のものです。
幸いなことに、この動作は、オーバーライドされたフィールドが親コンストラクターで使用されている場合にのみ明らかになります。それでは何が起こっているのかわかりにくいかもしれないので、ここで説明します。
それが問題になった場合は、フィールドの代わりにメソッドまたはゲッター/セッターを使用して修正できます。
高度な情報
初めてチュートリアルを読む場合は、このセクションは飛ばしても構いません。
それは継承とsuper
背後にある内部メカニズムに関するものです。
super
の内部をもう少し詳しく見てみましょう。途中でいくつかの興味深いものを見るでしょう。
まず最初に言っておきますが、これまで学んだことを総合すると、 super
機能することはまったく不可能です。
そうですね、技術的にどのように機能するのか自問してみましょう。オブジェクト メソッドが実行されると、現在のオブジェクトがthis
として取得されます。 super.method()
を呼び出す場合、エンジンは現在のオブジェクトのプロトタイプからmethod
を取得する必要があります。しかし、どうやって?
このタスクは簡単に見えるかもしれませんが、実際はそうではありません。エンジンは現在のオブジェクトthis
認識しているため、親method
this.__proto__.method
として取得できます。残念ながら、そのような「単純な」解決策は機能しません。
問題を示してみましょう。クラスを使用せず、単純にするためにプレーン オブジェクトを使用します。
詳細を知りたくない場合は、この部分をスキップして、以下の[[HomeObject]]
サブセクションに進んでください。それは害にはなりません。または、物事を深く理解したい場合は、読み続けてください。
以下の例では、 rabbit.__proto__ = animal
。それでは試してみましょう: rabbit.eat()
でthis.__proto__
を使用してanimal.eat()
を呼び出します。
動物 = { にしてみましょう 名前:「動物」、 食べる() { alert(`${this.name} は食べます。`); } }; ウサギ = { にしましょう __proto__: 動物、 名前:「ウサギ」、 食べる() { // それが super.eat() のおそらく動作方法です this.__proto__.eat.call(this); // (*) } }; うさぎ.食べる(); // ウサギが食べる。
行(*)
では、プロトタイプ ( animal
) からeat
を取得し、それを現在のオブジェクトのコンテキストで呼び出します。単純なthis.__proto__.eat()
現在のオブジェクトではなくプロトタイプのコンテキストで親のeat
実行するため、ここでは.call(this)
が重要であることに注意してください。
そして、上記のコードでは実際に意図したとおりに動作し、正しいalert
が得られています。
次に、もう 1 つのオブジェクトをチェーンに追加しましょう。物事がどのように壊れるかを見てみましょう:
動物 = { にしてみましょう 名前:「動物」、 食べる() { alert(`${this.name} は食べます。`); } }; ウサギ = { にしましょう __proto__: 動物、 食べる() { // ...ウサギスタイルでバウンスし、親 (動物) メソッドを呼び出します this.__proto__.eat.call(this); // (*) } }; let longEar = { __proto__: ウサギ、 食べる() { // ...長い耳で何かをし、親 (ウサギ) メソッドを呼び出します this.__proto__.eat.call(this); // (**) } }; longEar.eat(); // エラー: 最大呼び出しスタック サイズを超えました
コードはもう機能しません! longEar.eat()
を呼び出そうとしたときにエラーが発生していることがわかります。
それほど明白ではないかもしれませんが、 longEar.eat()
呼び出しを追跡すると、その理由がわかります。 (*)
と(**)
の両方の行で、 this
の値は現在のオブジェクト ( longEar
) です。これは重要です。すべてのオブジェクト メソッドは、プロトタイプなどではなく、現在のオブジェクトをthis
として取得します。
したがって、 (*)
と(**)
の両方の行で、 this.__proto__
の値はまったく同じです: rabbit
。どちらも無限ループのチェーンを上に行かずに、 rabbit.eat
を呼び出します。
何が起こるかの図は次のとおりです。
longEar.eat()
内の行(**)
は、 this=longEar
を指定してrabbit.eat
を呼び出します。
// longEar.eat() 内にはこれ = longEar があります this.__proto__.eat.call(this) // (**) // になります longEar.__proto__.eat.call(this) // つまり Rabbit.eat.call(this);
次に、 rabbit.eat
の行(*)
で、呼び出しをチェーンのさらに上位に渡したいと思いますが、 this=longEar
なので、 this.__proto__.eat
は再びrabbit.eat
なります。
// Rabbit.eat() 内にはこれも含まれます = longEar this.__proto__.eat.call(this) // (*) // になります longEar.__proto__.eat.call(this) // または (もう一度) Rabbit.eat.call(this);
…つまり、 rabbit.eat
それ以上上昇できないため、自分自身を無限ループ内に呼び出すことになります。
this
だけでは問題は解決できません。
[[HomeObject]]
この解決策を提供するために、JavaScript は関数にもう 1 つの特別な内部プロパティ[[HomeObject]]
を追加します。
関数がクラスまたはオブジェクト メソッドとして指定されている場合、その[[HomeObject]]
プロパティがそのオブジェクトになります。
次に、それをsuper
で使用して、親プロトタイプとそのメソッドを解決します。
最初にプレーンオブジェクトを使用して、それがどのように機能するかを見てみましょう。
動物 = { にしてみましょう 名前:「動物」、 Eat() { // Animal.eat.[[HomeObject]] == 動物 alert(`${this.name} は食べます。`); } }; ウサギ = { にしましょう __proto__: 動物、 名前:「ウサギ」、 Eat() { // Rabbit.eat.[[HomeObject]] == ウサギ super.eat(); } }; let longEar = { __proto__: ウサギ、 名前:「ロングイヤー」、 Eat() { // longEar.eat.[[HomeObject]] == longEar super.eat(); } }; // 正しく動作します longEar.eat(); // 長い耳は食べる。
[[HomeObject]]
の仕組みにより、意図したとおりに動作します。 longEar.eat
などのメソッドは、その[[HomeObject]]
を認識し、そのプロトタイプから親メソッドを取得します。 this
を使用せずに。
以前から知られているように、一般に関数は「無料」であり、JavaScript のオブジェクトにバインドされていません。したがって、オブジェクト間でコピーして、別のthis
で呼び出すことができます。
メソッドはオブジェクトを記憶しているため、 [[HomeObject]]
の存在そのものがその原則に違反します。 [[HomeObject]]
変更できないため、この絆は永遠です。
言語内で[[HomeObject]]
が使用される唯一の場所は、 super
です。したがって、メソッドがsuper
使用しない場合でも、それを無料とみなし、オブジェクト間でコピーできます。しかし、 super
の場合は問題が発生する可能性があります。
以下は、コピー後の間違ったsuper
リザルトのデモです。
動物 = { にしてみましょう SayHi() { alert(`私は動物です`); } }; // ウサギは動物から継承します ウサギ = { にしましょう __proto__: 動物、 SayHi() { super.sayHi(); } }; 植物にしてみましょう = { SayHi() { alert("私は植物です"); } }; // ツリーはプラントから継承します let ツリー = { __proto__: 植物、 SayHi: Rabbit.sayHi // (*) }; ツリー.sayHi(); // 私は動物です (?!?)
tree.sayHi()
を呼び出すと、「私は動物です」と表示されます。間違いなく間違っています。
理由は簡単です。
(*)
の行では、メソッドtree.sayHi
がrabbit
からコピーされました。コードの重複を避けたかっただけでしょうか?
その[[HomeObject]]
rabbit
で作成されたため、 rabbit
です。 [[HomeObject]]
変更する方法はありません。
tree.sayHi()
のコードにはsuper.sayHi()
が含まれています。 rabbit
から上がってanimal
からメソッドを引き継いでいます。
何が起こるかの図は次のとおりです。
[[HomeObject]]
クラスとプレーン オブジェクトの両方のメソッドに対して定義されます。ただし、オブジェクトの場合、メソッドは"method: function()"
ではなく、正確にmethod()
として指定する必要があります。
この違いは私たちにとっては重要ではないかもしれませんが、JavaScript にとっては重要です。
以下の例では、比較にメソッド以外の構文が使用されています。 [[HomeObject]]
プロパティが設定されていないため、継承が機能しません。
動物 = { にしてみましょう Eat: function() { // Eat() の代わりに意図的にこのように記述します {... // ... } }; ウサギ = { にしましょう __proto__: 動物、 食べる: function() { super.eat(); } }; うさぎ.食べる(); // スーパー呼び出しエラー ([[HomeObject]] がないため)
クラスを拡張するには: class Child extends Parent
:
つまり、 Child.prototype.__proto__
Parent.prototype
になるため、メソッドは継承されます。
コンストラクターをオーバーライドする場合:
this
使用する前に、 Child
コンストラクターで親コンストラクターをsuper()
として呼び出す必要があります。
別のメソッドをオーバーライドする場合:
Child
メソッドでsuper.method()
使用して、 Parent
メソッドを呼び出すことができます。
内部構造:
メソッドは、内部[[HomeObject]]
プロパティ内のクラス/オブジェクトを記憶します。これが親メソッドをsuper
解決する方法です。
したがって、 super
含むメソッドをあるオブジェクトから別のオブジェクトにコピーするのは安全ではありません。
また:
アロー関数には独自のthis
またはsuper
がないため、周囲のコンテキストに透過的に適合します。
重要度: 5
以下は、 Animal
拡張したRabbit
のコードです。
残念ながら、 Rabbit
オブジェクトは作成できません。どうしたの?修正してください。
クラス動物{ コンストラクター(名前) { this.name = 名前; } } クラス Rabbit extends Animal { コンストラクター(名前) { this.name = 名前; this.created = Date.now(); } } let Rabbit = new Rabbit("White Rabbit"); // エラー: これは定義されていません アラート(ウサギの名前);
これは、子コンストラクターがsuper()
を呼び出す必要があるためです。
修正されたコードは次のとおりです。
クラス動物{ コンストラクター(名前) { this.name = 名前; } } クラス Rabbit extends Animal { コンストラクター(名前) { スーパー(名前); this.created = Date.now(); } } let Rabbit = new Rabbit("White Rabbit"); // もういいよ アラート(ウサギの名前); // 白うさぎ
重要度: 5
Clock
クラスがあります。現時点では、時刻が毎秒出力されます。
クラスクロック{ コンストラクター({ テンプレート }) { this.template = テンプレート; } 与える() { let date = new Date(); let hours = date.getHours(); if (時間 < 10) 時間 = '0' + 時間; let mins = date.getMinutes(); if (分 < 10) 分 = '0' + 分; let secs = date.getSeconds(); if (秒 < 10) 秒 = '0' + 秒; 出力 = this.template にします .replace('h', 時間) .replace('m', 分) .replace('s', 秒); console.log(出力); } 停止() { clearInterval(this.timer); } 始める() { this.render(); this.timer = setInterval(() => this.render(), 1000); } }
Clock
を継承し、パラメーターprecision
(「ティック」間のms
数) を追加する新しいクラスExtendedClock
を作成します。デフォルトでは1000
(1 秒) です。
コードはファイルextended-clock.js
内にある必要があります。
元のclock.js
を変更しないでください。延長してください。
タスクのサンドボックスを開きます。
class ExtendedClock extends Clock { コンストラクター(オプション) { スーパー(オプション); let { 精度 = 1000 } = オプション; this.precision = 精度; } 始める() { this.render(); this.timer = setInterval(() => this.render(), this.precision); } };
サンドボックスでソリューションを開きます。