JavaScriptのプロトタイプ継承の具体例(.prototypeと__proto__)
JavaScriptのプロトタイプ継承の具体例を紹介します。
Constructor関数についてはこちらの記事を参照ください。
JavaScriptのプロトタイプ継承の仕組み
プロトタイプ継承の仕組みを理解するために、次のようなConstructor関数を使うこととします。
//Constructor関数
const Person = function(firstName, birthYear) {
//オブジェクトプロパティ
this.firstName = firstName;
this.birthYear = birthYear;
}
//インスタンスオブジェクト
const yamada = new Person('Yamada', 1988);
const sasaki = new Person('Sasaki', 2020);
const kondo = new Person('Kondo', 2020);
prototype
prototypeは、オブジェクトに存在する特別なプロパティです。
Constructor関数.prototypeをコンソールに表示してみます。
console.log(Person.prototype);
//{constructor: ƒ}
このように、PersonというConstructor関数にprototypeというプロパティがたしかに存在することがわかります。
プロトタイプ継承
プロトタイプ継承とは、プロトタイプを受け継いで、機能を流用できるようにすることです。
上で確認したprototypeプロパティにメソッドを追加することができます。
そうすることで、他のオブジェクトでもそのメソッドを使えるようになります。
たとえば、下のように、calc2050というメソッドを追加します。
//prototypeにメソッドを追加する
Person.prototype.calc2050 = function () {
console.log(2050 - this.birthYear);
};
//インスタンスでメソッドを使う
sasaki.calc2050(); //30
そうすると、sasakiインスタンスでもcalc2050というメソッドが使えるようになります。
SourcesのWatchで、インスタンスの__proto__というプロパティにメソッドが格納されていることが確認できます。
このように、コンストラクタ関数.prototypeでメソッドを追加すると、インスタンスの__proto__に参照先がコピーされて、インスタンスでもそのメソッドを使用することができます。
ちなみに、calc2050の中に書かれているthisキーワードは、使われるインスタンスごとに変わります。
sasakiインスタンスならsasakiがthisキーワードの対象になります。
Person.prototypeで追加されたプロパティはインスタンスのプロパティではない
calc2050というプロパティは、Person.prototypeにセットされていました。それをyamadaインスタンスが呼び出すとき、yamada.calc2050()という形で呼び出せるので、yamadaインスタンス自身のプロパティのように見えてしまいますが、そうではありません。JavaScriptは、yamadaインスタンスの__proto__プロパティを探してcalc2050というプロパティを発見することができます。
yamadaインスタンスは、calc2050プロパティをプロトタイプ継承によって引き継いで使えるようになっているということです。
yamadaインスタンスはcalc2050プロパティをyamadaインスタンスのプロトタイプ委譲しているという表現もできます。
Person.prototype.favoriteColor ='green';
console.log(sasaki,yamada);
console.log(yamada.favoriteColor);//green
console.log(yamada.hasOwnProperty('favoriteColor'));//false
favoriteColorはPersonのプロパティで、Personの__proto__の中に格納されています。
このようにアクセスができますが、faboriteColorはyamadaインスタンスのプロパティというわけではありません。
hasOwnPropertyを使うことで、yamadaインスタンス自身のプロパティではないということが確認できます。
インスタンス.__proto__とConstructor関数.prototypeの参照先の実体は同じ
sasaki.__proto__とPerson.prototypeの参照先の実体は同じです。
確認するためには、次のように確認したいオブジェクトに「.__proto__」と続けます。
console.log(sasaki.__proto__);// {calc2050: ƒ, constructor: ƒ}
console.log(sasaki.__proto__ === Person.prototype); //true
console.log(Person.prototype.isPrototypeOf(sasaki));//true
console.log(Person.prototype.isPrototypeOf(Person));//false
このように、sasakiの__proto__と、Person.prototypeが同じであることがわかります。
Person.prototypeはPersonのプロトタイプではない
ややこしい点になりますが、Person.prototypeはPersonのプロトタイプではありません。
上のPerson.prototype.isPrototypeOf(Person)でfalseを返していることから、Person.prototypeはPersonのプロトタイプではないということがいえます。
Person.prototypeは、インスタンスが使うためのプロトタイプです。
.prototypeというプロパティの名前が混乱の原因になるので、.prototypeは「リンクされているオブジェクトのプロトタイプ」というように置き換えて理解しておきましょう。
__proto__はなぜ使えるのか?
こちらの記事で、new演算子の「3. {}がprototypeにリンクされる」という特徴をあげています。
これで__proto__というプロパティが使えるのは、「{}がprototypeにリンクされる」という工程があったからです。
空のオブジェクトがprototypeに紐付けされたため、そのプロパティである__proto__プロパティが使えるようになります。
プロトタイプ継承のメリット
親のConstructor関数のprototypeにメソッドを追加すると、各インスタンスでそのメソッドが使えるようになることがわかりました。
親のConstructor関数に直接メソッドを記入ことでも、同じように各インスタンスでメソッドを使えるようにすることもできます。
しかし、親のConstructor関数に直接メソッドを記入すると、すべてのインスタンスにメソッドの実体がコピーされてしまい、インスタンスの数によっては膨大なメモリを消費することになってしまいます。
プロトタイプ継承では、インスタンス化した際に、prototypeの参照が__proto__にコピーされます。
メソッドの実体がコピーされているのではなく、参照先をコピーしていることになり、参照先の実体はコンストラクタ関数もインスタンスも同じものということになります。
このようにすることでメモリの効率化を図ることができるという点が、プロトタイプ継承のメリットです。
プロトタイプチェーン(prototype chain)
Personというコンストラクタ関数が、Person.prototype===yamadaオブジェクトのプロトタイプを保持しています。
そして、このPerson.prototypeというもの自体も、オブジェクトです。そして、JavaScriptではオブジェクトは必ずプロトタイプを持っています。
このPerson.prototypeのプロトタイプは、Object.prototypeというObjectというコンストラクタ関数が保持しているプロパティです。
このObjectというのは、JavaScirptのビルドインオブジェクトです。
ビルドインオブジェクト(build-in objects)とは、コード実行前にJavaScriptエンジンによって自動的に生成されるオブジェクトのことです。たとえば、String,Number, Function, Math, Date, Objectなどです。
特に、オブジェクトのインスタンスを生成するためのビルドインオブジェクトをビルドインコンストラクターとも言われます。
console.log(Person.prototype.__proto__ === Object.prototype);
//true
yamada,proto === Person.prototypeである。
Person.prototype.proto === Object.prototypeである。
というこの繋がりのことを、プロトタイプチェーンといいます。
console.log(Object.prototype.__proto__);//null
console.log(yamada.__proto__.__proto__.__proto__);//null
Object.prototypeはプロトタイプチェーンのトップになります。そのため、Object.prototype.__proto__はnullとなります。
また、yamadaオブジェクトから__proto__をチェーンしても、プロトタイプチェーンのトップにたどり着くので、nullになります。