プログラミングでは、何かを取り入れてそれを拡張したいと思うことがよくあります。
たとえば、プロパティとメソッドを備えたuser
オブジェクトがあり、その変形としてadmin
とguest
を作成したいとします。 user
にあるものを再利用したいのですが、そのメソッドをコピー/再実装するのではなく、その上に新しいオブジェクトを構築するだけです。
プロトタイプ継承は、これに役立つ言語機能です。
JavaScript では、オブジェクトには特別な隠しプロパティ[[Prototype]]
(仕様での名前) があり、これはnull
であるか、別のオブジェクトを参照します。そのオブジェクトは「プロトタイプ」と呼ばれます。
object
からプロパティを読み取り、そのプロパティが見つからない場合、JavaScript はプロトタイプから自動的にプロパティを取得します。プログラミングでは、これを「プロトタイプの継承」と呼びます。そしてすぐに、そのような継承の多くの例と、それに基づいて構築されたよりクールな言語機能を研究する予定です。
プロパティ[[Prototype]]
は内部にあり非表示ですが、設定する方法はたくさんあります。
そのうちの 1 つは、次のように特別な名前__proto__
使用することです。
動物 = { にしてみましょう 食べる:本当 }; ウサギ = { にしましょう ジャンプ:本当 }; ウサギ.__proto__ = 動物; // Rabbit.[[Prototype]] = 動物を設定します
ここで、 rabbit
からプロパティを読み取って、そのプロパティが見つからない場合、JavaScript は自動的にanimal
からそれを取得します。
例えば:
動物 = { にしてみましょう 食べる:本当 }; ウサギ = { にしましょう ジャンプ:本当 }; ウサギ.__proto__ = 動物; // (*) // これで、rabbit で両方のプロパティを見つけることができます。 アラート(うさぎ.食べる); // 真実 (**) アラート(rabbit.jumps); // 真実
ここで、行(*)
はanimal
rabbit
のプロトタイプに設定します。
次に、 alert
プロパティrabbit.eats
(**)
を読み取ろうとすると、そのプロパティはrabbit
内にないため、JavaScript は[[Prototype]]
参照に従い、 animal
内でそれを見つけます (下から上に見てください)。
ここで、「 animal
rabbit
の原型である」または「 rabbit
animal
の原型を継承している」と言えます。
したがって、 animal
便利なプロパティとメソッドがたくさんある場合、それらはrabbit
で自動的に使用できるようになります。このようなプロパティは「継承」と呼ばれます。
animal
にメソッドがある場合は、 rabbit
で呼び出すことができます。
動物 = { にしてみましょう 食べる:本当、 歩く() { alert("動物の散歩"); } }; ウサギ = { にしましょう ジャンプ: true、 __proto__: 動物 }; // ウォークはプロトタイプから取得されます うさぎ.walk(); // 動物の散歩
メソッドは次のようにプロトタイプから自動的に取得されます。
プロトタイプ チェーンは長くなる場合があります。
動物 = { にしてみましょう 食べる:本当、 歩く() { alert("動物の散歩"); } }; ウサギ = { にしましょう ジャンプ: true、 __proto__: 動物 }; let longEar = { 耳の長さ: 10、 __proto__: ウサギ }; // ウォークはプロトタイプチェーンから取得されます longEar.walk(); // 動物の散歩 アラート(longEar.jumps); // true (ウサギから)
ここで、 longEar
から何かを読み取って、それが見つからない場合、JavaScript はrabbit
でそれを探し、次にanimal
でそれを探します。
制限事項は 2 つだけです。
参照を循環させることはできません。 __proto__
サークル内に割り当てようとすると、JavaScript はエラーをスローします。
__proto__
の値は、オブジェクトまたはnull
のいずれかになります。他のタイプは無視されます。
また、明らかなことかもしれませんが、 [[Prototype]]
1 つだけ存在できます。オブジェクトは他の 2 つのオブジェクトから継承することはできません。
__proto__
[[Prototype]]
の履歴ゲッター/セッターです。
これら 2 つの違いが分からないのは、初心者の開発者にありがちな間違いです。
__proto__
内部の[[Prototype]]
プロパティと同じではないことに注意してください。これは[[Prototype]]
のゲッター/セッターです。後ほど、これが重要となる状況を見ていきますが、今は JavaScript 言語の理解を深めていく際に、このことを念頭に置いておきましょう。
__proto__
プロパティは少し古いです。これは歴史的な理由から存在しており、最新の JavaScript では、プロトタイプを取得/設定する代わりにObject.getPrototypeOf/Object.setPrototypeOf
関数を使用する必要があると提案されています。これらの関数についても後で説明します。
仕様により、 __proto__
ブラウザーでのみサポートされなければなりません。ただし、実際には、サーバー側を含むすべての環境が__proto__
サポートしているため、非常に安全に使用できます。
__proto__
表記の方が直感的にわかりやすいため、例ではそれを使用します。
プロトタイプはプロパティの読み取りにのみ使用されます。
書き込み/削除操作はオブジェクトを直接操作します。
以下の例では、独自のwalk
メソッドをrabbit
に割り当てます。
動物 = { にしてみましょう 食べる:本当、 歩く() { /* このメソッドは Rabbit では使用されません */ } }; ウサギ = { にしましょう __proto__: 動物 }; Rabbit.walk = function() { alert("ウサギ!バウンスバウンス!"); }; うさぎ.walk(); // うさぎ!弾む弾む!
今後、 rabbit.walk()
呼び出しは、プロトタイプを使用せずに、オブジェクト内でメソッドをすぐに見つけて実行します。
アクセサー プロパティは例外で、代入はセッター関数によって処理されます。したがって、そのようなプロパティへの書き込みは、実際には関数の呼び出しと同じです。
そのため、 admin.fullName
以下のコードで正しく動作します。
ユーザー = { にします 名前:「ジョン」、 姓:「スミス」、 set fullName(値) { [この名前、この姓] = value.split(" "); }、 フルネームを取得() { `${this.name} ${this.surname}`を返します。 } }; 管理者 = { にしましょう __proto__: ユーザー、 isAdmin: true }; アラート(管理者.フルネーム); // ジョン・スミス (*) // セッタートリガー! admin.fullName = "アリス・クーパー"; // (**) アラート(管理者.フルネーム); // アリス・クーパー、管理者の状態が変更されました アラート(ユーザー.フルネーム); // John Smith、ユーザー保護の状態
ここの行(*)
では、プロパティadmin.fullName
プロトタイプuser
にゲッターがあるため、それが呼び出されます。そして、行(**)
では、プロパティのプロトタイプにセッターがあるため、それが呼び出されます。
上記の例では、 set fullName(value)
内のthis
の値は何なのかという興味深い疑問が生じるかもしれません。 this.name
およびthis.surname
プロパティはどこに書き込まれますか: user
またはadmin
ですか?
答えは簡単です。 this
プロトタイプの影響をまったく受けません。
メソッドがオブジェクトまたはそのプロトタイプのどこにあるかは関係ありません。メソッド呼び出しでは、 this
常にドットの前のオブジェクトです。
したがって、セッター呼び出しadmin.fullName=
、 user
ではなくadmin
this
として使用します。
これは実際には非常に重要なことです。なぜなら、多くのメソッドを備えた大きなオブジェクトがあり、それを継承するオブジェクトがある可能性があるからです。そして、継承オブジェクトが継承メソッドを実行すると、ビッグ オブジェクトの状態ではなく、自身の状態のみが変更されます。
たとえば、ここではanimal
「メソッドストレージ」を表しており、 rabbit
それを利用しています。
rabbit.sleep()
の呼び出しは、 rabbit
オブジェクトにthis.isSleeping
設定します。
// 動物にはメソッドがあります 動物 = { にしてみましょう 歩く() { if (!this.isSleeping) { アラート(`私は歩きます`); } }、 寝る() { this.isSleeping = true; } }; ウサギ = { にしましょう 名前:「白ウサギ」、 __proto__: 動物 }; // Rabbit.isSleeping を変更します うさぎ.sleep(); アラート(rabbit.isSleeping); // 真実 アラート(animal.isSleeping); // 未定義 (プロトタイプにはそのようなプロパティはありません)
結果の画像:
animal
を継承する他のオブジェクト ( bird
、 snake
など) がある場合、それらもanimal
のメソッドにアクセスできるようになります。ただし、各メソッド呼び出しのthis
は、 animal
ではなく、呼び出し時 (ドットの前) に評価される、対応するオブジェクトになります。したがって、 this
にデータを書き込むと、データはこれらのオブジェクトに保存されます。
その結果、メソッドは共有されますが、オブジェクトの状態は共有されません。
for..in
ループは、継承されたプロパティも反復処理します。
例えば:
動物 = { にしてみましょう 食べる:本当 }; ウサギ = { にしましょう ジャンプ: true、 __proto__: 動物 }; // Object.keys は独自のキーのみを返します アラート(オブジェクト.キー(ウサギ)); // ジャンプします // for..in は独自のキーと継承されたキーの両方をループします for(ウサギに小道具を入れよう)alert(小道具); // ジャンプしてから食べる
それが望ましくなく、継承されたプロパティを除外したい場合は、組み込みメソッド obj.hasOwnProperty(key) があります。 obj
にkey
という名前の独自の (継承されていない) プロパティがある場合にtrue
を返します。
したがって、継承されたプロパティをフィルターで除外することができます (または、継承されたプロパティに対して何か他のことを行うことができます)。
動物 = { にしてみましょう 食べる:本当 }; ウサギ = { にしましょう ジャンプ: true、 __proto__: 動物 }; for(ウサギに小道具を入れます) { let isOwn = Rabbit.hasOwnProperty(prop); if (isOwn) { alert(`私たちの: ${prop}`); // 私たちの: ジャンプ } それ以外 { アラート(`継承: ${prop}`); // 継承: 食べる } }
ここには次の継承チェーンがあります。 rabbit
、 Object.prototype
を継承するanimal
を継承し( animal
リテラルオブジェクト{...}
であるため、デフォルトです)、その上にnull
配置します。
注意してください、面白いことが 1 つあります。 rabbit.hasOwnProperty
メソッドはどこから来たのでしょうか?私たちがそれを定義したわけではありません。チェーンを見ると、メソッドがObject.prototype.hasOwnProperty
によって提供されていることがわかります。つまり、遺伝するのです。
…しかし、 for..in
継承されたプロパティをリストするのに、なぜhasOwnProperty
、 eats
やjumps
のようにfor..in
ループに現れないのでしょうか?
答えは簡単です。数え切れないからです。 Object.prototype
の他のすべてのプロパティと同様に、これにはenumerable:false
フラグがあります。そしてfor..in
列挙可能なプロパティのみをリストします。このため、このプロパティと残りのObject.prototype
プロパティはリストされていません。
他のほとんどすべてのキー/値取得メソッドは、継承されたプロパティを無視します。
Object.keys
、 Object.values
など、他のほとんどすべてのキー/値取得メソッドは、継承されたプロパティを無視します。
これらはオブジェクト自体に対してのみ作用します。プロトタイプのプロパティは考慮されません。
JavaScript では、すべてのオブジェクトに、別のオブジェクトまたはnull
のいずれかである非表示の[[Prototype]]
プロパティがあります。
obj.__proto__
使用してアクセスできます (歴史的なゲッター/セッター、他の方法もありますが、すぐに説明します)。
[[Prototype]]
で参照されるオブジェクトを「プロトタイプ」と呼びます。
obj
のプロパティを読み取ったり、メソッドを呼び出したりしたいときに、それが存在しない場合、JavaScript はプロトタイプ内でそれを見つけようとします。
書き込み/削除操作はオブジェクトに直接作用し、プロトタイプは使用しません (セッターではなくデータ プロパティであると仮定します)。
obj.method()
を呼び出し、 method
がプロトタイプから取得された場合でも、 this
obj
を参照します。したがって、メソッドは継承された場合でも常に現在のオブジェクトで動作します。
for..in
ループは、それ自体のプロパティとその継承されたプロパティの両方を反復します。他のすべてのキー/値取得メソッドは、オブジェクト自体に対してのみ動作します。
重要度: 5
以下は、オブジェクトのペアを作成し、それらを変更するコードです。
プロセスではどの値が表示されますか?
動物 = { にしてみましょう ジャンプ: null }; ウサギ = { にしましょう __proto__: 動物、 ジャンプ:本当 }; アラート(rabbit.jumps); //? (1) Rabbit.jumps を削除します。 アラート(rabbit.jumps); //? (2) 動物のジャンプを削除します。 アラート(rabbit.jumps); //? (3)
答えは 3 つあるはずです。
true
、 rabbit
から取られました。
null
、 animal
から取得されます。
undefined
、そのようなプロパティはもうありません。
重要度: 5
このタスクには 2 つの部分があります。
次のオブジェクトがあるとします。
頭 = { にしてみましょう メガネ:1 }; テーブル = { にします ペン: 3 }; 寝ましょう = { シート:1、 枕: 2 }; ポケット = { にしましょう お金: 2000 };
__proto__
使用して、プロパティ検索がパスpockets
→ bed
→ table
→ head
に従うようにプロトタイプを割り当てます。たとえば、 pockets.pen
3
( table
にある)、 bed.glasses
1
( head
にある) である必要があります。
質問に答えてください: glasses
pockets.glasses
として取得するのとhead.glasses
として取得するのはどちらが早いですか?必要に応じてベンチマークを行います。
__proto__
追加しましょう:
頭 = { にしてみましょう メガネ: 1 }; テーブル = { にします ペン:3、 __proto__: 頭 }; 寝ましょう = { シート:1、 枕:2、 __proto__: テーブル }; ポケット = { にしましょう お金: 2000、 __proto__: ベッド }; アラート(ポケット.ペン); // 3 アラート(ベッド.メガネ); // 1 アラート(テーブル.マネー); // 未定義
最新のエンジンでは、パフォーマンスの点で、プロパティをオブジェクトから取得するか、そのプロトタイプから取得するかに違いはありません。彼らはプロパティが見つかった場所を記憶し、次のリクエストでそれを再利用します。
たとえば、 pockets.glasses
の場合、 glasses
見つけた場所 ( head
内) を記憶しており、次回はそこを検索します。また、何か変更があった場合に内部キャッシュを更新する機能も備えているため、安全に最適化できます。
重要度: 5
animal
から引き継いでrabbit
がいます。
rabbit.eat()
呼び出した場合、 animal
とrabbit
のどちらのオブジェクトがfull
プロパティを受け取りますか?
動物 = { にしてみましょう 食べる() { this.full = true; } }; ウサギ = { にしましょう __proto__: 動物 }; うさぎ.食べる();
答えはrabbit
です。
これは、 this
ドットの前のオブジェクトであるため、 rabbit.eat()
rabbit
変更するためです。
プロパティの検索と実行は別のものです。
メソッドrabbit.eat
は最初にプロトタイプで見つかり、次にthis=rabbit
で実行されます。
重要度: 5
一般的なhamster
オブジェクトから継承したspeedy
とlazy
2 つのハムスターがあります。
片方に餌をあげると、もう片方もお腹がいっぱいになります。なぜ?どうすれば修正できますか?
ハムスター = { とします 胃: []、 食べる(食べ物) { this.stomach.push(食べ物); } }; スピーディにしましょう = { __プロト__: ハムスター }; 怠けてみましょう = { __プロト__: ハムスター }; // これは食べ物を見つけました Speedy.eat("リンゴ"); アラート(スピーディ.胃); // りんご // これにもありますが、なぜですか?修正してください。 アラート(怠惰な胃); // りんご
speedy.eat("apple")
の呼び出しで何が起こっているのかを注意深く見てみましょう。
メソッドspeedy.eat
プロトタイプ ( =hamster
) 内にあり、 this=speedy
(ドットの前のオブジェクト) で実行されます。
次に、 this.stomach.push()
stomach
プロパティを見つけて、それに対してpush
を呼び出す必要があります。 this
中でstomach
を探しますが( =speedy
)、何も見つかりませんでした。
次に、プロトタイプのチェーンに従い、 hamster
のstomach
を見つけます。
次に、 push
を呼び出して、プロトタイプの胃に食べ物を追加します。
つまり、すべてのハムスターは 1 つの胃を共有しているのです。
lazy.stomach.push(...)
とspeedy.stomach.push()
の両方の場合、プロパティstomach
プロトタイプ内で見つかり(オブジェクト自体には存在しないため)、新しいデータがプロトタイプにプッシュされます。
単純な代入this.stomach=
の場合、そのようなことは起こらないことに注意してください。
ハムスター = { とします 胃: []、 食べる(食べ物) { // this.stomach.push ではなく this.stomach に割り当てます this.stomach = [食べ物]; } }; スピーディにしましょう = { __プロト__: ハムスター }; 怠けてみましょう = { __プロト__: ハムスター }; // 早い人は食べ物を見つけた Speedy.eat("リンゴ"); アラート(スピーディ.胃); // りんご // 怠け者のお腹は空っぽ アラート(怠惰な胃); // <何もしない>
this.stomach=
stomach
の検索を実行しないので、これですべてが正常に機能します。値はthis
オブジェクトに直接書き込まれます。
また、各ハムスターに独自の胃があることを確認することで、この問題を完全に回避できます。
ハムスター = { とします 胃: []、 食べる(食べ物) { this.stomach.push(食べ物); } }; スピーディにしましょう = { __プロト__: ハムスター、 胃: [] }; 怠けてみましょう = { __プロト__: ハムスター、 胃: [] }; // 早い人は食べ物を見つけた Speedy.eat("リンゴ"); アラート(スピーディ.胃); // りんご // 怠け者のお腹は空っぽ アラート(怠惰な胃); // <何もしない>
一般的な解決策として、 stomach
の上のような特定のオブジェクトの状態を記述するすべてのプロパティをそのオブジェクトに書き込む必要があります。これにより、そのような問題が防止されます。