吉本集の個人ブログ
Web制作の技術について書いています。たまに日記も書きます。

【JS入門】オブジェクトについて Vol.4(プロトタイプチェーン、コンストラクタ)

2018年1月21日 / category : javascript

前回の記事ではプロトタイプオブジェクトについて解説しました。引き続きプロトタイプオブジェクトについて解説していきます。今回はプロトタイプの特長のひとつであるプロトタイプチェーンの仕組みについて解説します。

プロトタイプチェーンとは

次のコードを見てみます。

sample.js
function Test(){};
let t = new Test();
let str = t.toString();
console.log( str ); // [object Object]
console.log( typeof str ); // string

上のコードを見ると、変数strには[object Object]という値(データ)が代入されています。typeof演算子で型を確認すると、この値(データ)は文字列だということがわかります。Testオブジェクトのインスタンスである変数tにメソッドtoStringを実行すると、[object Object]という文字列が返ってきます。メソッドtoStringは、値(データ)を文字列として返すメソッドです。
このメソッドtoStringは、Testオブジェクトでは定義されていません。このメソッドはどこからきたメソッドでしょうか。

メソッドtoStringは、Objectオブジェクトのプロトタイプで定義されているメソッドです。
次のコードで確かめてみます。

sample.js
function Test(){};
let t = new Test();
console.log( t.toString === Object.prototype.toString ); // true

変数tは、Testオブジェクトのインスタンスです。このインスタンスは、前回の記事「オブジェクトについて Vol.3(プロトタイプオブジェクト)」で解説した通りTestオブジェクトのプロトタイプ、Objectオブジェクトのプロトタイプを継承しています。
オブジェクトがプロパティ(メソッド)を参照した際、そのオブジェクト自身にそのプロパティが定義していなかった場合、継承元のオブジェクトのプロパティを参照しにいく仕組みをプロトタイプチェーンと呼びます。継承元がnullになったタイミング(Objectオブジェクトまで到達し。それでも見つからなかった場合)、そこでプロトタイプチェーンの参照がストップし、undefinedを返します。
上のコードを見ると、インスタンスがメソッドtoStringを実行していますが、インスタンス自身にメソッドtoStringは定義されていません。プロトタイプチェーンによって継承元(プロトタイプ)のプロパティを参照しにいき、Testオブジェクトのプロトタイプで定義されているメソッドtoStringが実行されていることになります。
ただ、上のコードを見ると、TestオブジェクトにはメソッドtoStringは定義していません。これは、Testオブジェクトを定義した際に、Objectオブジェクトのプロトタイプを継承しており、プロパティ(メソッド)をオーバーライド(上書き)されているため、TestオブジェクトにメソッドtoStringは定義されていることになります。
図にすると次のようになります。

以上が、プロトタイプチェーンの仕組みについてです。
続けて、前回の記事で少し触れたプロパティconstructorについて解説します。

コンストラクタ(constructor)とは

以前「コンストラクタ関数について」という記事を書きましたので、コンストラクタという言葉は耳にしています。オブジェクトを定義した際に定義されるプロパティのコンストラクタ(constructor)と、コンストラクタ関数とは、どういう関係でしょうか。次のコードを見てみます。

sample.js
function Test(){};
console.log( Test.constructor ); // ƒ Function() { [native code] }
console.log( Test.constructor === Function ); // true

Testオブジェクトのプロパティconstructorを参照すると、Functionオブジェクトが代入されていました。
図にしてみます。

続いて、Testオブジェクトのインスタンスを生成し、そのインスタンスのプロパティconstructorが何を参照しているか確かめてみます。

sample.js
function Test(){};
let t = new Test();
console.log( t.constructor ); // ƒ Test(){}
console.log( t.constructor === Test ); // true

Testオブジェクトのインスタンスのプロパティconstructorは、Testオブジェクトを参照していることがわかりました。
図にしてみます。

この結果からプロパティconstructorは、プロトタイプで継承しているオブジェクトを参照していることがわかります。わかりやすいように上の図にプロパティ__proto__も追加してみます。

上の図から、Testオブジェクトのインスタンスのプロトタイプは、Testオブジェクトのプロトタイプを継承しています。
このインスタンスのプロパティconstructorは、Testオブジェクトを参照しています。
同様の関係性で、Testオブジェクトのプロトタイプは、Functionオブジェクトのプロトタイプを継承しています。
Testオブジェクトのプロパティconstructorは、Functionオブジェクトを参照しています。

また、以前解説した「コンストラクタ関数について」の記事で使用したコンストラクタ関数と、プロパティconstructorの関係性を見てみると、同じ関数を参照していることもわかります。

sample.js
function Test(){};
let t = new Test(); // コンストラクタ関数
console.log( t.constructor === Test ); // プロパティconstructorは、コンストラクタ関数を参照している

それでは続けて、Testオブジェクトのプロトタイプのプロパティconstructorを見てみます。

sample.js
function Test(){};
console.log( Test.prototype.constructor ); // ƒ Test(){}
console.log( Test.prototype.constructor === Test ); // true

この結果は想像できたでしょうか。TestオブジェクトのインスタンスのプロトタイプはTestオブジェクトのプロトタイプを参照しているので、同じ結果になります。よって、TestオブジェクトのプロトタイプのプロパティconstructorはTestオブジェクトとなります。言い換えると、オブジェクトのプロトタイプのプロパティconstructorはそのオブジェクトを参照すると言えます。

それでは続けて、Functionオブジェクトのプロパティconstructor、Functionオブジェクトのプロトタイプのプロパティconstructorを見てみます。

sample.js
console.log( Function.constructor ); // ƒ Function() { [native code] }
console.log( Function.constructor === Function ); // true
console.log( Function.prototype.constructor ); // ƒ Function() { [native code] }
console.log( Function.prototype.constructor === Function ); // true

Functionオブジェクトのプロトタイプのプロパティconstructorが、Functionオブジェクトを参照するというのは、先ほどの「オブジェクトのプロトタイプのプロパティconstructorはそのオブジェクトを参照する」という関係性から想像できます。

FunctionオブジェクトのプロパティconstructorがFunctionオブジェクトを参照するのは、前回の記事「オブジェクトについて Vol.3(プロトタイプオブジェクト)」で、関数オブジェクトのプロトタイプの最終地点はFunctionオブジェクトのプロトタイプになると解説しました。プロパティconstructorも同様で、関数オブジェクトのプロパティconstructorの最終地点もFunctionオブジェクトになります。
図にしてみます。

それでは最後に、Objectオブジェクトのプロパティconstructor、Objectオブジェクトのプロトタイプのプロパティconstructorを見てみます。

sample.js
console.log( Object.constructor ); // ƒ Function() { [native code] }
console.log( Object.constructor === Function ); // true
console.log( Object.prototype.constructor ); // ƒ Object() { [native code] }
console.log( Object.prototype.constructor === Object ); // true

Objectオブジェクトについても、前回の記事「オブジェクトについて Vol.3(プロトタイプオブジェクト)」のプロトタイプの関係を見ると想像できたかと思います。
Objectオブジェクトのプロパティconstructorは、Functionオブジェクトになります。これはObjectオブジェクトのプロトタイプがFunctionオブジェクトのプロトタイプを参照していることから想像できました。
Objectオブジェクトのプロトタイプのプロパティconstructorは、Objectオブジェクトになります。これは、プロトタイプの継承元の最終地点がObjectオブジェクトのプロトタイプであることから想像できました。
図にしてみます。

以上が、コンストラクタについてになりますが、まとめると全てのオブジェクトにはプロパティconstructorを保持しています。このプロパティconstructorはコンストラクタ関数を参照します。コンストラクタ関数はFunctionオブジェクトですので、コンストラクタ関数の最終継承元はFunctionオブジェクトになります。

上の図を見ると少々複雑に見えますが、プロトタイプとコンストラクタの関係性については重要なポイントになります。