プログラミングでは、しばしば何かを拡張したい場合があります。
例えば、プロパティとメソッドを持つ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…) を使用してください。