継承とプロトタイプチェーン
はじめに
これからオブジェクトのプロトタイプについての説明が続きますが、前の記事でも書いたように、ES2015 以降 class キーワードが導入され、クラスベースに近いコードが許されるようになりました。これはシンタックスシュガー(糖衣構文:ある構文を簡略化したり可読性をよくするための記法)であり、あくまでもJavaScript はプロトタイプベース言語(厳密にはプロトタイプに基づいたオブジェクトベースの言語)に変わりありません。
class キーワードが導入されたためオブジェクトのプロトタイプについて深く知識がなくても、オブジェクト指向プログラミングはできます。しかし、テクニカルな部分の知識を持つことはプログラミングをする上で、アドバンテージとなります。オブジェクトを理解することは、JavaScript を理解する上で大きなウェイトをしめます。この記事では詳しいところまで掘り下げませんが、記事下部に MDN のオブジェクトに関する記事のリンクがありますので、一読して理解を深められることをお勧めします。
プロパティの継承
JavaScript のオブジェクトはプロトタイプと呼ばれる、他のオブジェクトへの繋がりを持っています。
let person = {
age : 10 ,
fname : 'Ippei' ,
lname : 'Kimura' ,
profile : function() { console.log( 'name:' + this.lname + ' ' + this.fname + ' age:' + this.age ) ; } ,
} ;
console.log( person ) ;7行目でオブジェクト person の内容をコンソールに出力しています。
// console.log( person ) ; の結果
{age: 10, fname: 'Ippei', lname: 'Kimura', profile: ƒ}このようにコンソールに表示されます。
オブジェクトの内容がそのまま表示されています。もっと詳しく見るためにコンソールの内容をクリックして開いてみます。
// console.log( person ) ; の結果
{age: 10, fname: 'Ippei', lname: 'Kimura', profile: ƒ}
age: 10
fname: "Ippei"
lname: "Kimura"
profile: ƒ ()
[[Prototype]]: Object <--- 作成した覚えのないプロパティがある。!!7行目に person オブジェクトで作成していないプロパティ [[Prototype]]: Object が作成されています。
[[Prototype]]: Object を開いてみると constructor や toString 等の関数が多数作成されています。
let person = {
age : 10 ,
fname : 'Ippei' ,
lname : 'Kimura' ,
profile : function() { console.log( 'name:' + this.lname + ' ' + this.fname + ' age:' + this.age ) ; } ,
} ;
console.log( person.toString() ) ; // [object Object]person.toString() を実行してみると person オブジェクトを文字列に変換した [object Object] が表示されます。
「これは Object のプロトタイプを person オブジェクトが継承しているからです。」と、よく説明されるのですが、はっきり言ってよくわかりません。
指定されたプロトタイプオブジェクトとプロパティから、新しいオブジェクトを生成する、Object.create() メソッドを使って person オブジェクトから新しいオブジェクトを生成してみます。
ちょっと解りにくいかもしれませんが、ほんの少しだけ我慢してお付き合いください。
let person = {
age : 10 ,
fname : 'Ippei' ,
lname : 'Kimura' ,
profile : function() { console.log( 'name:' + this.lname + ' ' + this.fname + ' age:' + this.age ) ; } ,
} ;
let kato = Object.create( person ) ; // person オブジェクトから新しいオブジェクト kato を作成
console.log( kato ) ; // {} <-- 空のオブジェクト?
kato.profile() ; // name:Kimura Ippei age:10 <-- person の内容が引き継がれている?person オブジェクトから Object.create() メソッドによって作成された新しいオブジェクト kato は空のオブジェクトであるのに kato.profile() を実行すると「name:Kimura Ippei age:10」と表示されます。これは person オブジェクトの内容が kato オブジェクトに引き継がれているからです。
kato オブジェクトは空のオブジェクトに見えますが、kato オブジェクトの [[Prototype]]: Object には、しっかりと person オブジェクトの内容が引き継がれていました。
// console.log( kato ) ; の結果
{} <--- kato オブジェクト
[[Prototype]]: Object <--- kato オブジェクトのプロトタイプは person オブジェクトを継承
age: 10 ---+
fname: "Ippei" + <--- person オブジェクトのプロパティを継承しています。
lname: "Kimura" +
profile: ƒ () ---+
[[Prototype]]: Objectkato オブジェクトのプロトタイプは person オブジェクトを継承(参照)し、person オブジェクトのプロトタイプは Object() を継承(参照)しているのです。

つまり、 kato.profile() は person オブジェクトのプロパティを継承しているので、内部的には person.profile() プロパティを実行しています。
これがプロトタイプチェーン(プロパティの継承)です。
この例だけでは、kato オブジェクトは person オブジェクトの別名でしかありません。
次の例では、プロトタイプチェーンの代表的な動きがみられます。
let person = {
age : 10 ,
fname : 'Ippei' ,
lname : 'Kimura' ,
profile : function() { console.log( 'name:' + this.lname + ' ' + this.fname + ' age:' + this.age ) ; } ,
} ;
let kato = Object.create( person ) ;
kato.lname = 'Kato' ; // <--- *1 kato オブジェクトの lname プロパティに 'kato' を設定する。
kato.age = 22 ; // <--- *2 kato オブジェクトの age プロパティに 22 を設定する。
console.log( kato ) ; // {lname: 'Kato', age: 22} <-- kato オブジェクトは、設定したプロパティだけを持っている
kato.profile() ; // name:Kato Ippei age:22 <--- *3
console.log( kato.toString() ) ; // [object Object] <--- *4*1と*2で kato オブジェクトのプロパティに値を設定しています。
kato.lname = 'Kato' ; // <--- *1 kato オブジェクトの lname プロパティに 'kato' を設定する。 kato.age = 22 ; // <--- *2 kato オブジェクトの age プロパティに 22 を設定する。
これにより、kato オブジェクトはプロパティ lname と age を持ちます。
// console.log( kato ) ; の結果
{lname: 'Kato', age: 22} <--- kato オブジェクトには設定した lname と age プロパティが設定されている。
age: 22
lname: "Kato"
[[Prototype]]: Object <--- kato オブジェクトのプロトタイプは person オブジェクトを継承
age: 10
fname: "Ippei" <--- kato オブジェクトに設定されていない fname と profile プロパティは person オブジェクトから継承する。
lname: "Kimura"
profile: ƒ ()
[[Prototype]]: Object*3で実行される、kato.profile() は「name:Kato Ippei age:22」と表示します。プロパティ lname と age は kato オブジェクトから fname と profile は person オブジェクトからプロトタイプチェーンにより継承されます。
*4で実行される kato.toString() の toString() は Object からの継承です。

プロトタイプチェーンはオブジェクト間の親子関係を示すようなものです。
子オブジェクト( kato オブジェクト )にプロパティが存在するかを調べ、存在しなければ親オブジェクト( person オブジェクト )に存在するか調べ、存在しなければ祖父オブジェクト( Object() ) に存在するか調べます。そこまで (プロトタイプがnullになるまで) 遡ってなければ undefined を返します。
let person = {
age : 10 ,
fname : 'Ippei' ,
lname : 'Kimura' ,
profile : function() { console.log( 'name:' + this.lname + ' ' + this.fname + ' age:' + this.age ) ; } ,
} ;
let kato = Object.create( person ) ;
kato.lname = 'Kato' ;
kato.age = 22 ;
for( let k in kato ) { // kato オブジェクトのプロパティを取得
console.log(k) ; // lname age fname profile
}
参考リンク
MDN 開発者向けのウェブ技術 > 標準組み込みオブジェクト > Object
MDN 開発者向けのウェブ技術 > JavaScript オブジェクト入門> Object のプロトタイプ
MDN 開発者向けのウェブ技術 > Object.create()
MDN 開発者向けのウェブ技術 > オブジェクトでの作業
MDN 開発者向けのウェブ技術 > JavaScript「再」入門
