JavaScript これでプロトタイプチェーンの総まとめ

JavaScriptで理解が難しいプロトタイプチェーンの理解が出来る様に、わかりやすくいろいろな例を挙げて説明しています。 まず[[Prototype]]と__proto__は同じものです。 また内部プロパティとプロパティは異なります。

[[Prototype]]===__proto__はtrue

まずこれを知ると理解しやすくなります。 プロトタイプチェーンを難解にしている要因の一つに、同じような用語が3つ出てきます。[[Prototype]]とprototypeと__proto__です。 しかし基本[[Prototype]]と__proto__は同じなので、[[Prototype]]とprototypeだけを理解すればいいのです。

  • 基本、[[Prototype]]===__proto__はtrueです。 (ここで『[[Prototype]]===__proto__』である説明。) なので、__proto__の説明は[[Prototype]]を理解したらいいのです。
  • [[Prototype]]は、内部プロパティと呼ばれる隠れたプロパティで、通常言われるプロパティとはことなり、二重角括弧で表されます。 オブジェクトに無いプロパティやメソッドが呼ばれたら[[Prototype]]内を参照します。 隠れたプロパティなので内部プロパティと呼ばれ、隠れているので直接読み書きできません。
  • prototypeプロパティは、[[Prototype]]とは別物で通常のプロパティ。 prototypeプロパティは継承するメソッドやプロパティを入れておくプロパティです。 継承して生成した新しいオブジェクトに、継承するメソッドやプロパティの 参照リンクが[[Prototype]]入ります。 これで、継承して生成したオブジェクトは、継承元のメソッド等にアクセスできるのです。 prototypeプロパティは隠れプロパティでないので直接読み書きできます。
  • 内部プロパティ[[Prototype]]もprototypeプロパティも、実際保持している値の型はオブジェクトの{}型なのです。 {}の型と同様に参照リンクがコピーされるだけで、実体(中身や値)がコピーされて継承されるわけではありません。
  • この下に更に詳しく動作説明しています。

内部プロパティ[[Prototype]]

  • オブジェクト、配列、コンストラクタで作られた関数オブジェクトの内部に持つ隠れた内部プロパティ。
  • 記述方法は[[Prototype]]で先頭文字のPは大文字。
  • 内部プロパティ[[Prototype]]とprototypeプロパティとは別のもの。
  • 継承元のprototypeのリンクが内部プロパティ[[Prototype]]に入る。(この下の例は簡単にするために
  • 存在しないプロパティ呼ばれた時、内部プロパティ[[Prototype]]には存在あるいは継承元に存在する場合、内部プロパティ[[Prototype]]を参照して持ってくる。
  • プロパティのバックアップの様なもの。
  • [[Prototype]]はObject.getPrototypeOf()とObject.setProtoytpeOf()で読み書きする。
  • [[Prototype]]は継承されない。

下のコードとその下の図で簡単な例で動作説明。

const func_a =function(name){this.name=name}; // ConstructorのFunctionオブジェクトを作ります。
func_a.prototype.c=12;                        // prototypeプロパティを設定。
func_a.prototype.func1=function(){console.log('wow')};// prototypeプロパティを設定。

const func_c = new func_a('ichiri');         //func_aを継承元にしてfunc_cを生成。

console.log(func_c);            //-> func_a { name: 'ichiri' }  cやfunc1は存在しません。
console.log(func_c.c);          //-> 12    しかしcは12となります。
func_c.func1();                 //-> wow   func1も呼び出せます。
console.log(Object.getPrototypeOf(func_c)); //-> { c: 12, func1: [Function (anonymous)] }
                                            //-> cもfunc1も[[Prototype]]に参照が入っています。
console.log(func_a.prototype===Object.getPrototypeOf(func_c)); //-> true
        //->継承元のprotoにある参照が継承先の[[Prototype]]にコピーされているので同じになります。
console.log(func_a.prototype);              //-> { c: 12, func1: [Function (anonymous)] }
//************************************************************************
var obj_a ={name:'apple'};                  // Object literalでも試してみます。
console.log(obj_a.type);                    //-> undefined  obj_a.typeは存在しません。
console.log(Object.getPrototypeOf(obj_a));  //-> [Object: null prototype] {}
                         //obj_aに[[Prototype]]は存在しません。
var obj_b ={type:'Fruits'}    
Object.setPrototypeOf(obj_a,obj_b);         //ojb_aに[[Prototype]]にtypeを設定します。
console.log(obj_a.type);                    //-> Fruits   obj_a.typeが読めました。
console.log(Object.getPrototypeOf(obj_a));  // { type: 'Fruits' } 
                         //obj_aに[[Prototype]]が存在しています。
obj_a.type='food';                          //obj_aにtypeプロパティ作成。
console.log(obj_a.type);                    //-> food 作ったプロパティ優先。
console.log(Object.getPrototypeOf(obj_a));  // { type: 'Fruits' }  [[Prototype]]はまだ存在している。

[[Prototype]]のメリット

  • メソッドやプロパティが大きくても参照なので省メモリ。
  • しかも、ひな型のオブジェクトを継承する事で、同じ継承元のプロパティやメソッドを複数のオブジェクトに実装しなくても良いので効率がよい。

prototypeプロパティ

Function.prototypeやclass.prototypeプロパティの事です。(オブジェクトや配列やコンストラクタで作られていないFunctionオブジェクトにはprototypeプロパティを設定しても、newで構築できないので実際使えません。) prototypeプロパティは、あるひな型Functionオブジェクトやclassから、新しいオブジェクトやインスタンスを生成する際、継承する内容を書いておくプロパティです。 その内容を参照するリンクが、新しいオブジェクトの[[Prototype]]に格納されます。 prototypeプロパティには以下の内容が入っています。

  • メソッド (constructorと自作関数)—これもプロパティですが…
  • プロパティ(階層化可能な自作プロパティ)

prototypeプロパティの中を見ると、constructor()も見えますが、constructor()はFunctionオブジェクトやclassオブジェクトで決まった動作をする名前のメソッドなので、実質メソッドとプロパティがprototypeプロパティに格納できます。

prototypeは日本語に直訳すると試作品です。 試作品を作って、量産では試作品をひな型としてそのコピーを沢山作ります。 このprototypeプロパティの中にある メソッド・プロパティが継承先から参照できるようになるのでコピーを大量生産できるようになるのです。

var func_a =function(name){this.name=name};
func_a.prototype.c=12;                                 // prototypeプロパティを設定。
func_a.prototype.func1=function(){console.log('wow')}; // prototypeプロパティを設定。

const func_c = new func_a('ichiri');

console.log(func_a.prototype);                 //-> { c: 12, func1: [Function (anonymous)] }
console.log(Object.getPrototypeOf(func_c));    //-> { c: 12, func1: [Function (anonymous)] } 
                                               // prototypeから参照を継承しているので同じ。
console.log(func_a.prototype===Object.getPrototypeOf(func_c));  //-> true   
                                               // 参照先を継承しているので同じ。
func_c.func1();                                //-> wow   
// func_cオブジェクトにはfunc1メソッドは無いが、継承元を参照してfunc1を呼び出している
console.log(func_c.c);                         //-> 12  
// func_cオブジェクトにはcプロパティは無いが、継承元を参照してcを呼び出している。

obj_d={d:111};
Object.setPrototypeOf(func_c,obj_d);         // func_c.[[Prototype]]に{d:111}を設定すると、
console.log(Object.getPrototypeOf(func_c));  // 他の設定(参照)が消えた。
  • [[Prototype]]は参照専用なので書き換えは推奨されていません。 上の例でも、func_c.[[Prototype]]の設定を一つでも書くと、他の全ての参照リンクが消えてしまいます。 部分的にも追加する事はできないので、参照(読み出し)専用にするのが良いと思います。
  • prototypeプロパティは量産から参照される元なので直接書き換えたり追加できます。
  • prototypeプロパティに定義したメソッドやプロパティは、prototypeプロパティに参照のリンクが出来、new構文で作成する新しいオブジェクトの内部プロパティ[[Prototype]]に同じ参照のリンク(参照)が格納される。
  • Javascriptでは多重継承はできず、1つの元となるオブジェクトからの継承しかサポートしていない。
console.log(Function.prototype === Object.__proto__);            //-> true
console.log(Function.prototype.__proto__ === Object.prototype);  //-> true
console.log(Object.prototype.__proto__);     //-> null 非推奨

[[Prototype]]と__proto__

Functionオブジェクトの直下[[Prototype]]と__proto__は同じものです。

同じ参照元を参照している

  • b.[[Prototype]]とb.__proto__は中身が同じです。
  • a.prototypeを変更すると、b.[[Prototype]]とb.__proto__も同じように変わります。
  • そしてb.[[Prototype]]はa.prototypeを参照しています。
  • 『Mdm web docs 継承とプロトタイプチェーン』で同等と書かれています。
var a =function(name){this.name=name};
a.prototype.c=12;
a.prototype.func1=function(){console.log('wow')};
const b = new a('ichiri');

console.log(a.prototype);              //-> { c: 12, func1: [Function (anonymous)] }
console.log(Object.getPrototypeOf(b)); //-> { c: 12, func1: [Function (anonymous)] } // a.prototype参照なのでa.prototypeと同じ
console.log(b.__proto__);              //-> { c: 12, func1: [Function (anonymous)] } // 同様にa.prototypeを参照している
a.prototype.c=23;                                                                    // 参照元のa.cを23に変更
console.log(Object.getPrototypeOf(b)); //-> { c: 23, func1: [Function (anonymous)] } // [[Prototype]]も__proto__も
console.log(b.__proto__);              //-> { c: 23, func1: [Function (anonymous)] } // 同様に23になった。

継承先のオブジェクトの[[Prototype]]を変更してはいけない

  • 上記コードに以下コードを付け足しました。
  • setPropertyOfでbの[[Prototype]]にa.propertyに無いtypeとfunc2を設定しました。
  • すると、bの[[Prototype]]mob.__proto__も設定された値だけになりました。
  • b.aで先ほど23とa.prototype.aを参照していたのですが、undefinedで参照していません。
  • b.__proto__.aで設定するとbの[[Prototype]]mob.__proto__もb.aが10で見えるようになりました。
  • これは継承先の[[Prototype]]を一つでも設定すると、継承元のprototypeの設定が全て見えなくなるという事です。
  • 『Mdm web docsのObject.setPrototypeOf()』では、このsetPropertyOf()は速度が非常に遅い事と、継承元の参照ができなくなりプログラムが正常動作しなくなるかもしれないので使用はしないべきと書かれています。 どうしても新しい[[Prototype]]が必要なオブジェクトがある場合はObject.setPropertyOf()よりObject.create()を使うようにと注意書きされています。
console.log(a.prototype);               //-> { a: 23, func1: [Function (anonymous)] }
console.log(Object.getPrototypeOf(b));  //-> { a: 23, func1: [Function (anonymous)] }
console.log(b.__proto__);               //-> { a: 23, func1: [Function (anonymous)] }
b.func1();                              //-> wow
console.log(b.a);                       //-> 23
// ---------ここから変更---------------------------------------------
Object.setPrototypeOf(b,{type:'red',func2(){console.log('yeah')}}); //bの[[Prototype]]設定
console.log(Object.getPrototypeOf(b));  //-> { type: 'red', func2: [Function: func2] }
console.log(b.__proto__);               //-> { type: 'red', func2: [Function: func2] }
console.log(b.a);                       //-> undefined
b.__proto__.type='white';
b.__proto__.a=10;
console.log(Object.getPrototypeOf(b));  //-> { type: 'white', func2: [Function: func2], a: 10 }
console.log(b.__proto__);               //-> { type: 'white', func2: [Function: func2], a: 10 }
console.log(a.prototype);               //-> { a: 23, func1: [Function (anonymous)] }
console.log(b.a);                       //-> 10
b.func1();                              //-> TypeError: b.func1 is not a function

おまけ1 Functionオブジェクトとclassオブジェクトの違い

違いはprototypeへのメソッドの格納方法だけ

super()が使えるclassの方が便利ですが、ここでは継承と言う観点だけでまとめます。

  • 動作は同じになりますが、違いはprototypeの直下にFunction(メソッド)があるかどうかです。
  • Functionオブジェクトのfunc_a.prototypeにはfunc1:Function(メソッド)が表示されます。
  • classオブジェクトのclass_b.prototypeにはfunc1:Function(メソッド)が表示されません。
  • これはclassオブジェクトのfunc1は、class_b.prototypeの下のconstructorの中に入っているからです。
  • Functionオブジェクトのfunc1は、func_a.prototypeの直下にあり、constructorと同列です。
var func_a =function(name){this.name=name};
func_a.prototype.c=12;
func_a.prototype.func1=function(){console.log('wow')};

class class_b {
  constructor(name){
    this.name = name;
  }
  func1() {
    console.log("yummy"); 
  }
}
class_b.prototype.a=123;

console.log(func_a);                         //-> [Function: func_a]
console.log(class_b);                        //-> [class class_b]
console.log(func_a.prototype);               //-> { c: 12, func1: [Function (anonymous)] }
console.log(class_b.prototype);              //-> { a: 123 }
console.log(Object.getPrototypeOf(func_a));  //-> {}   継承していないので何も入っていません
console.log(Object.getPrototypeOf(class_b)); //-> {}  継承していないので何も入っていません
console.log(func_a.__proto__);               //-> {}  [[Prototype]]なので上同様何も入っていません
console.log(class_b.__proto__);              //-> {}  [[Prototype]]なので上同様何も入っていません
console.log(Object.getPrototypeOf(func_a)===func_a.__proto__);    //=> true [[Prototype]]=== __proto__
console.log(Object.getPrototypeOf(class_b)===class_b.__proto__);  //=> true [[Prototype]]=== __proto__

継承してもfunctionオブジェクトと動作は同じだが[[Prototype]]の見え方が違う

  • constructorによって実体として作られたnameはどちらも格納されている。
  • 実体として持っていないfunc1メソッドも、どちらも継承元を参照して実行できる。
  • 実体として持っていないプロパティも、どちらも継承元を参照している。
  • [[Prototype]]を見ると、元のオブジェクトのprototypeと同じ内容が表示される。 どちらもつまり継承元のprototypeを参照している。
var func_a =function(name){this.name=name};
func_a.prototype.c=12;
func_a.prototype.func1=function(){console.log('wow')};

class class_b {
  constructor(name){
    this.name = name;
  }
  func1() {
    console.log("yummy"); 
  }
}
class_b.prototype.a=123;

const func_c = new func_a('ichiri');
const class_d = new class_b('apple');

console.log(func_c.name);       //-> ichiri
console.log(class_d.name);      //-> apple
func_c.func1();                 //-> wow
class_d.func1();                //-> yummy
console.log(func_c.c);          //-> 12
console.log(class_d.a);         //-> 123
console.log(Object.getPrototypeOf(func_c));   //-> { c: 12, func1: [Function (anonymous)] }
console.log(Object.getPrototypeOf(class_d));  //-> { a: 123 }
console.log(func_c.prototype);  //-> undefined
console.log(class_d.prototype); //-> undefined

しかし、見え方が違う。 

VScodeで見ると違いはconstructorの上か下かとfunc1の色が濃いいかどうかだけでちゃんと参照している。 多分、もっと細かい動作の違いがあるのかもしれないけど、単純な継承では動作は同じだった。

おまけ2 VScodeで見える__proto__は何?

[[Prototype]]===__proto__なので、VScodeで[[Prototype]]が表示されているのは分かるのですが、Functionオブジェクトやclassオブジェクト内を見ると、[[Prototype]]とは別に__proto__も現れます。

  • しかし、その__proto__は[[Prototype]]の下の、更に[[Prototype]]の下にしかありません。
  • なので実際使われないと思います。
  • しかも値がnullで何もしていません。
  • その上、いつでもオブジェクト名.__proto__===オブジェクト名.[[Prototype]]はいつもtrueです。
  • しかもオブジェクト名.__proto__はオブジェクトの直下の__proto__の事です。
  • オブジェクトの直下の__proto__は表示されてなく、[[Prototype]]が表示されています。
  • VScodeで見える、nullの__proto__は、オブジェクト名.[[Prototype]].[[Prototype]].__proto__なので異なる__proto__ですね。
  • しかも__proto__は非推奨になっているので、
  • 無視していいのだと思います。

おまけ3 プリミティブ型には[[Prototype]]は使えない

エラーにはならないですが、プリミティブ型(number)には[[Prototype]]は使えませんでした。

var a='0';
a.name='ichiri';
var obj1 = {type: "human"};
Object.setPrototypeOf(a, obj1);   
console.log(a.name);                   //-> undefined
console.log(Object.getPrototypeOf(a)); //-> {}
console.log(a.type);                   //-> undefined

コメント