プログラミングでは、しばしば何かを拡張したい場合があります。
例えば、プロパティとメソッドを持つuserオブジェクトがあり、それを少し変更したバリアントとしてadminとguestを作成したいとします。 userにあるものをコピー/再実装するのではなく、再利用して、その上に新しいオブジェクトを構築したいと考えています。
_プロトタイプ継承_は、それを助ける言語機能です。
[[Prototype]]
JavaScriptでは、オブジェクトには特別な非表示プロパティ[[Prototype]](仕様書に記載されている名前)があり、それはnullか別のオブジェクトを参照しています。そのオブジェクトは「プロトタイプ」と呼ばれます。
objectからプロパティを読み込んで、それが存在しない場合、JavaScriptは自動的にプロトタイプから取得します。プログラミングでは、これは「プロトタイプ継承」と呼ばれます。そしてすぐに、そのような継承の多くの例と、それに基づいて構築されたよりクールな言語機能を学習します。
プロパティ[[Prototype]]は内部的で非表示ですが、それを設定する方法はたくさんあります。
そのうちの1つは、次のように特別な名前__proto__を使用することです。
let animal = {
eats: true
};
let rabbit = {
jumps: true
};
rabbit.__proto__ = animal; // sets rabbit.[[Prototype]] = animal
これで、rabbitからプロパティを読み込んで、それが存在しない場合、JavaScriptは自動的にanimalから取得します。
例えば
let animal = {
eats: true
};
let rabbit = {
jumps: true
};
rabbit.__proto__ = animal; // (*)
// we can find both properties in rabbit now:
alert( rabbit.eats ); // true (**)
alert( rabbit.jumps ); // true
ここで、行(*)はanimalをrabbitのプロトタイプとして設定します。
次に、alertがプロパティrabbit.eats (**)を読み取ろうとすると、それはrabbitにないため、JavaScriptは[[Prototype]]参照をたどり、animalで見つけます(下から上を見てください)。
ここでは、「animalはrabbitのプロトタイプです」または「rabbitはanimalからプロトタイプ的に継承します」と言うことができます。
そのため、animalに多くの便利なプロパティとメソッドがある場合、それらはrabbitで自動的に利用可能になります。このようなプロパティは「継承された」と呼ばれます。
animalにメソッドがある場合、それをrabbitで呼び出すことができます。
let animal = {
eats: true,
walk() {
alert("Animal walk");
}
};
let rabbit = {
jumps: true,
__proto__: animal
};
// walk is taken from the prototype
rabbit.walk(); // Animal walk
メソッドは、次のようにプロトタイプから自動的に取得されます。
プロトタイプチェーンはもっと長くすることができます。
let animal = {
eats: true,
walk() {
alert("Animal walk");
}
};
let rabbit = {
jumps: true,
__proto__: animal
};
let longEar = {
earLength: 10,
__proto__: rabbit
};
// walk is taken from the prototype chain
longEar.walk(); // Animal walk
alert(longEar.jumps); // true (from rabbit)
これで、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に割り当てています.
let animal = {
eats: true,
walk() {
/* this method won't be used by rabbit */
}
};
let rabbit = {
__proto__: animal
};
rabbit.walk = function() {
alert("Rabbit! Bounce-bounce!");
};
rabbit.walk(); // Rabbit! Bounce-bounce!
これ以降、rabbit.walk()呼び出しは、プロトタイプを使用せずに、オブジェクト内のメソッドをすぐに見つけて実行します.
アクセサプロパティは例外です。割り当てはセッター関数によって処理されるためです。そのため、そのようなプロパティへの書き込みは、実際には関数を呼び出すのと同じです.
そのため、以下のコードではadmin.fullNameは正しく機能します.
let user = {
name: "John",
surname: "Smith",
set fullName(value) {
[this.name, this.surname] = value.split(" ");
},
get fullName() {
return `${this.name} ${this.surname}`;
}
};
let admin = {
__proto__: user,
isAdmin: true
};
alert(admin.fullName); // John Smith (*)
// setter triggers!
admin.fullName = "Alice Cooper"; // (**)
alert(admin.fullName); // Alice Cooper, state of admin modified
alert(user.fullName); // John Smith, state of user protected
ここで、行(*)のプロパティadmin.fullNameにはプロトタイプuserにゲッターがあるため、それが呼び出されます。そして、行(**)のプロパティにはプロトタイプにセッターがあるため、それが呼び出されます.
「this」の値
上記の例では、興味深い質問が生じる可能性があります。set fullName(value)内の`this`の値は何ですか?プロパティ`this.name`と`this.surname`はどこに書き込まれますか? userまたはadminに?
答えは簡単です。`this`はプロトタイプの影響をまったく受けません.
メソッドがどこで見つかるかは関係ありません。オブジェクトまたはそのプロトタイプにあります。メソッド呼び出しでは、`this`は常にドットの前のオブジェクトです.
そのため、セッター呼び出しadmin.fullName =は、userではなくadminを`this`として使用します.
これは実際には非常に重要なことです。なぜなら、多くのメソッドを持つ大きなオブジェクトがあり、それから継承するオブジェクトを持つことができるからです。そして、継承するオブジェクトが継承されたメソッドを実行すると、それらは大きなオブジェクトの状態ではなく、自分の状態のみを変更します。
たとえば、ここではanimalは「メソッドストレージ」を表し、rabbitはそれを使用します。
呼び出しrabbit.sleep()は、rabbitオブジェクトにthis.isSleepingを設定します。
// animal has methods
let animal = {
walk() {
if (!this.isSleeping) {
alert(`I walk`);
}
},
sleep() {
this.isSleeping = true;
}
};
let rabbit = {
name: "White Rabbit",
__proto__: animal
};
// modifies rabbit.isSleeping
rabbit.sleep();
alert(rabbit.isSleeping); // true
alert(animal.isSleeping); // undefined (no such property in the prototype)
結果の図
animalから継承するbird、snakeなどの他のオブジェクトがある場合、それらもanimalのメソッドにアクセスできます。ただし、各メソッド呼び出しの`this`は、呼び出し時(ドットの前)に評価された対応するオブジェクトであり、animalではありません。そのため、`this`にデータを書き込むと、これらのオブジェクトに格納されます。
その結果、メソッドは共有されますが、オブジェクトの状態は共有されません。
for…inループ
for..inループは、継承されたプロパティも反復処理します.
例えば
let animal = {
eats: true
};
let rabbit = {
jumps: true,
__proto__: animal
};
// Object.keys only returns own keys
alert(Object.keys(rabbit)); // jumps
// for..in loops over both own and inherited keys
for(let prop in rabbit) alert(prop); // jumps, then eats
それが望まないものであり、継承されたプロパティを除外したい場合は、組み込みメソッドobj.hasOwnProperty(key)があります。 objにkeyという名前の独自の(継承されていない)プロパティがある場合、trueを返します。
そのため、継承されたプロパティを除外することができます(または、それらを使用して何か他のことを行うことができます)。
let animal = {
eats: true
};
let rabbit = {
jumps: true,
__proto__: animal
};
for(let prop in rabbit) {
let isOwn = rabbit.hasOwnProperty(prop);
if (isOwn) {
alert(`Our: ${prop}`); // Our: jumps
} else {
alert(`Inherited: ${prop}`); // Inherited: eats
}
}
ここでは、次の継承チェーンがあります。rabbitはanimalから継承し、animalはObject.prototypeから継承します(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ループは、独自の プロパティと継承されたプロパティの両方を反復処理します。他のすべてのキー/値取得メソッドは、オブジェクト自体でのみ動作します.
コメント
<code>タグを使用し、複数行の場合は<pre>タグで囲み、10 行を超える場合はサンドボックス (plnkr、jsbin、codepen…) を使用してください。