2021/06/28

【JavaScript】プロトタイプチェーンについて徹底解説 !!

@ 古曳 純基

JavaScriptReact

はじめに

どうもどうも、じゅんきです。
今回はJavaScriptを学ぶ上で必ず通る道である「プロトタイプチェーン」について解説していくっ!
コードはできるだけ少なくして、図を多めにしたので初学者の方でも概念的理解ができると思います。

メンタルモデル

いつもながら、メンタルモデルを構築してから本題に入っていきましょう!
JSにおいて、全てのオブジェクトはプロトタイプという継承元のオブジェクトに対する参照を持っていて、それが鎖のようにつながっている構造のことをプロトタイプチェーンと言いいます。
そして、プロトタイプチェーンという構造をベースとすることで、オブジェクト毎にメソッドを定義する必要性がなくなるのでメモリが少なくて済むという点があります。

プロトタイプベースとクラスベース

JavaScriptを勉強する中でこれらのワードは聞いたことがあるかもしれません。これらはオブジェクト指向の2つのベースです。

(1)オブジェクト指向

オブジェクト指向ってそもそもなんなの?ってとこから話していきます。オブジェクト指向は、プログラミングでシステムを作成するときに、オブジェクトという単位で区切って、認識・再現することです。
そんなオブジェクト指向ですが、以下で説明する2つの考え方があります。

(2) クラスベース

クラスベースとは、全てのオブジェクトには雛形が存在しており、雛形をコピーすることによってインスタンスを作るというものです。
よく例えられるのが、たい焼きの型とたい焼きの実体の関係です。
オブジェクト指向と聞くと、私はこちらを想像します。

クラスベースには人間にとって直感的でわかりやすいというメリットの反面、インスタンスごとにメソッドを持つ性質からメモリをたくさん使ってしまうというデメリットがあります。

(3) プロトタイプベース

プロトタイプベースとは、オブジェクトは継承元のオブジェクトへの参照を持つというものです。こうすることで、オブジェクト毎にメソッドを持たせなくても、継承元を探索することで継承元の持つメソッドを実行することができます。
JSにおいて、厳密にいうとクラスは存在していません。なぜなら、JSはプロトタイプベースだからです。

みんなの声:「だけど、JS内でクラスの定義や継承とかできるじゃん?」

僕:「確かにそうですね!でもあれは、クラスを作成してるように見せかけてるシンタックスシュガーでしかないのですよ...」

みんなの声:「ま、ま、まじすかっ?!」

僕:「マジなんです。クラス構文はES2015から追加された機能なんですよ〜。あくまでJSはプロトタイプオブジェクト指向なんです」

妄想にお付き合いいただきありがとうございます。
プロトタイプベースのメリットは、オブジェクト毎に同じメソッドを持たないのでメモリ領域が少なくて済むという点です。
ですが、継承元がそもそも消えてしまえば、いくら参照を持ってるとは言えどもメソッドの実行はできません。
そいう言った意味で、人間にとって非直感であると言えます。

プロトタイプとは

先程からプロトタイプという言葉が飛び交っていましたが、そもそもプロトタイプってなんやねん?という疑問の声が聞こえます。
プロトタイプについて軽く説明していくっ!
以下MDNからの参照です。(https://developer.mozilla.org/ja/docs/Learn/JavaScript/Objects/Object_prototypes

プロトタイプは、JavaScript オブジェクトが互いに機能を継承するメカニズムです。

オブジェクトの継承が行われた際に、オブジェクト同士(継承元のオブジェクトと継承後のオブジェクト)がお互いに参照を持つことでお互いの機能を共有できるというある種Win-Win的なメカニズムだよってことが言いたいんだと思います。

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

やってまいりました、JSを学習しているときっと見かけるワード「プロトタイプチェーン」!!
これを理解していると、JSのクラス構文や関数の定義においてどんなことが起きているのかがわかるようになります。
こちらもMDNからの引用をもとに説明します。(https://developer.mozilla.org/ja/docs/Web/JavaScript/Inheritanceandthe_prototype_chain

JavaScript には1つだけ、継承が発生する要素があります。オブジェクトです。どのオブジェクトもプロトタイプと呼ばれる、他のオブジェクトへの内部的な繋がりを持っています。そのプロトタイプオブジェクトも自身のプロトタイプを持っており、あるオブジェクトのプロトタイプが null に到達するまでそれが続きます。 null は、定義によれば、プロトタイプを持たず、プロトタイプチェーンの最終リンクとなります。


これを要約すると、JSにおいてオブジェクトはプロトタイプという内部的つながりを持っており、その繋がりが鎖のようにさまざまなオブジェクトに続いているということです。つまり、オブジェクトのプロトタイプをみていくとそのオブジェクトの大元の継承もとに辿り着くということです。
ですが、継承の大元であるオブジェクトは初めから存在するものなので、そいつの継承元はない。つまり、nullに帰属するということになります。
もうちょっと踏み込むと、{ }(空オブジェクト)を経てから、nullに帰着します。

重要な概念なのでしっかりと押さえておきましょう。

グローバルオブジェクト

JavaScriptにおいて唯一のオブジェクトであるグローバルオブジェクトが存在しています。ブラウザという環境ではwindowオブジェクトがグローバルオブジェクトになります。
グローバルコンテキスト(グローバル空間)で変数を定義したとき、その変数はグローバルオブジェクトのプロパティになるという性質があります。
以下のコードについて、考察していきましょう。

//グローバル空間での変数定義
const Hoge = function (name ){ //function式による関数定義
  this.name = name ;
}
Hoge.prototype.hoge1 = "hogehoge";

//new演算子によるコンストラクタ関数の呼び出し
const foo = new Hoge("foo");

Hoge.prototype.hoge2 = "fugafuga";
const piyo = new Hoge("piyo");


なんのこっちゃって感じのコードですね。図を使って何が起きてるか説明していきます。
まず、最初の行ですがHogeという変数をグローバルコンテキストで定義しているので、グローバルオブジェクトのHogeプロパティに値が格納されます。図を以下に示します。
なお、青丸はオブジェクトを示しており、矢印はそのオブジェクトのプロパティを示しています。

FunctionとFunctionオブジェクトの関係

上のコードを理解するためには、この2つのオブジェクトの関係を知る必要があります。
プロトタイプチェーンを学習していてごっちゃ混ぜになりやすいワードがこの「Function」と「Functionオブジェクト」です。どちらも似たような名前ですが、意味が違うので理解しましょう。

(1)Function

少し余談になりますが、ユーザー側で定義しなくてもあらかじめ定義されているオブジェクトを標準組み込みオブジェクトと言います。
MDNによると基本オブジェクトには以下のようなものがあるようです。(https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects)


おっと、リストの上から2番目に「Function」がありますね!
そうなんです、Functionとは標準組み込みオブジェクトの一種なんです!
つまり、図で表すと以下のようになります。

(2)Functionオブジェクト

MDNの説明を以下に示します。(https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Function)

JavaScript の関数は、実際にはすべて Function オブジェクトです。

Functionオブジェクトとは、Functionをもとに生成されたオブジェクトのことです。
function式やコンストラクタを使って関数を定義すると、オブジェクトを生成するのですが、このオブジェクトをFunctionオブジェクトと言います。

今回のコードの場合、Hogeにはfunction式によって定義した関数が格納されているため、言い換えるとFunctionオブジェクトを格納しているとも言えます。また、Function自身も自分から生成されたFunctionオブジェクトを持っているんです。


また、全てのFunctionオブジェクトはprototypeプロパティを持っています。そして、prototypeプロパティの先にはなんらかのオブジェクトがセットされています。通常、関数オブジェクトを定義した直後にはprototypeプロパティにセットされているオブジェクトは中身が空っぽです(プロパティを持たない)。ですが、何か新しいオブジェクトを代入したり、新しいプロパティを追加したりする処理を実行することが可能です。
図を以下に示します。

Objectってなに?

ある程度、FunctionとFunctionオブジェクトについて理解を深めていただけたと思うので、続いてはObjectオブジェクトについて説明します。
Objectも先程のMDNの標準組み込みオブジェクトのリストの1番目に載っていたと思います。つまり、Objectも標準組み込みオブジェクトの一種ということになります。

MDNの解説によると、Objectとは以下に示すようなものなようです。

JavaScript のほぼすべてのオブジェクトが Object のインスタンスです。一般的なオブジェクトは、プロパティを (メソッドを含めて) Object.prototype から継承していますが、これらのプロパティはシャドウ化 (別名オーバーライド) されている場合があります。しかし、意図的にそうではない Object を生成したり (例えば Object.create(null) によって)、変更した結果そうではなくなる場合 (例えば Object.setPrototypeOf) もあります。
Object プロトタイプオブジェクトへの変更は、その変更の対象となるプロパティやメソッドがプロトタイプチェーンに沿ってさらにオーバーライドされない限り、プロトタイプチェーンを通してすべてのオブジェクトに表示されます。これはとても強力ですが、オブジェクトの動作をオーバーライドしたり拡張したりするのは潜在的に危険をはらむ仕組みでもあります。


JSにおいてのオブジェクトとはObjectのインスタンスであり、そのObjectのプロトタイプオブジェクトに対する変更は全てのオブジェクトに対して反映されてしまうと言った内容が書かれています。

また、MDNの引用において、Object.prototypeとあることからわかるようにObjectオブジェクトはFunctionオブジェクトなんです。
今の状況を図で示すと以下のようになります。

全てのオブジェクトは__proto__プロパティを持つ

全てのオブジェクトは、__proto__プロパティを持っており、これによって継承元のオブジェクトを参照しています。

__proto__プロパティを持つことのメリット

この記事の冒頭でもお話ししましたが、あるオブジェクトがメソッドを持っていなくても継承もとのメソッドを使用することができる点です。プリミティブ型のデータに対して、任意のメソッドが使えるのも__proto__のおかげなのです(プリミティブに対してオブジェクトのメソッドが使えるのは、ラッパーオブジェクトという概念が関わってくるので説明は端折らせてください)。

__proto__に持つ継承もとには以下の3つの規則があります。

(1) Functionオブジェクト.__proto__はFunction.prototypeである

図にすると以下のようになります。

HogeはtoStringというメソッドを持っていないが__proto__プロパティを参照することでFunctionオブジェクト.prototypeを見ることができ、さらにその中にtoStringというメソッドが定義されているため、そのメソッドを使うことができます。
しかし、次のような場合には注意が必要である。

この場合、使用されるのはメソッド1です。あなたがメソッド2の使用を意図している場合、この挙動はあなたを苦しめるでしょう。
基本、一番近い継承元のメソッドや値が優先して使われます。ですが解決方法はあります。以下のようにすればいいのです。

HogeのFunctionオブジェクトのprototypeはからの状態なのでそこにメソッドを追加すれば、こちらのメソッドを優先して使用できるようになります。

(2) Functionオブジェクト.__proto__.__proto__はObject.prototypeである

Functionオブジェクトの継承元がどこまで続くのか気になりますね。そこで、Functionオブジェクト.__proto__に対して__proto__をもう一度指定してやりましょう。そうすると、Object.prototypeにつながります。図を以下に示します。

さらに、深い継承元を見てみましょう。Functionオブジェクト.__proto__.__proto__.__proto__言い換えるとObject.prototype.__proto__にアクセスします。そうすると、どうなると思いますか?
答えはnullです。冒頭でも説明しましたが、継承の大元にあるオブジェクトの継承元はありません。だから、nullとなります。
図に示すと以下のようになります。

(3)ObjectもFunctionも継承もと(__proto__)はFunction .prototypeである

先程まではユーザー定義していた変数のFunctionオブジェクトについて考えました。
では、FunctionとObjectの継承元はどうなるのでしょうか?

結論は、どちらもFunction.prototypeにつながっています。
図を以下に示します。

この辺りからこんがらがってきたと思います。
「Functionオブジェクトがprototypeプロパティを持つ」のと「全てのオブジェクトは__prototype__プロパティによって継承元への参照を持つ」という構造が混乱の原因だと思いますが、この2つの決まりを覚えておけばなんとなく理解できてくると思います。

コードの考察続き

プロトタイプチェーンの全貌がなんとなく見えてきたと思うのでここからは、初めに紹介したコードの考察の続きをします。
念のために同じコードを以下に示します。

//グローバル空間での変数定義
const Hoge = function (name ){ //function式による関数定義
  this.name = name ;
}
Hoge.prototype.hoge1 = "hogehoge";

//new演算子によるコンストラクタ関数の呼び出し
const foo = new Hoge("foo");

Hoge.prototype.hoge2 = "fugafuga";
const piyo = new Hoge("piyo");


まず、function式によって関数の定義が行われています。これによってFunctionオブジェクトが生成され、変数Hogeにぶち込まれています。このとき、Functionから生成したFunctionオブジェクトのprototypeはデフォルトでは、new Object()というコンストラクターによって生成されたオブジェクトなので、__proto__プロパティはObject.prototypeを参照しているという形になります。
Hogeはグローバルコンテキスト上で定義されているのでグローバルオブジェクトのプロパティとなります。
ここまでをまとめると以下のようになります。

全てのFunctionオブジェクトの__proto__がFunctionオブジェクト.prototypeにつながってるのなんか面白いですね。

次に、Hogeのprototypeに対して、新しいプロパティと値を設定してますね。図は以下のようになります。


そして、new演算子を使ってコンストラクタ関数の呼び出しをしています。
コンストラクタ関数とは、「プロトタイプオブジェクトを継承してオブジェクトインスタンスを生成するための関数」のことです。
そのため、生成されたインスタンスの継承元はHoge.prototypeになっているのです。
変数fooはグローバルコンテキストなので、もちろんグローバルオブジェクトのプロパティとなります。
図で示すと以下のようになります。

次に、先ほどと同様にHoge.prototypeに対して新しいプロパティと値をセットしていますね。
そして、2つ目のインスタンスを生成しています。
図で示すと以下のようになります。

このコードで結局何が言いたかったのかというと、fooインスタンスもpiyoインスタンスもプロトタイプチェーンによってhoge1,hoge2にアクセスできるということです。
また、シンプルにコンストラクタ関数が何をしたいのかというのをイメージを持って理解してほしかったという点からコードの考察をしてみました。
決まりを覚えていると、プロトタイプチェーンがどういう構造になっているのかある程度、頭の中で整理できるようになってくると思います。

最後に

ここまで読んでみてどうでしたか?
多分、わかったようなわかってないようなって感じだと思います。僕もそんな状態ですが、何度か記事を読んだり、MDNの文章を読んでるうちに感覚的にわかってきた気がしています。
下に参考文献を載せておくので興味のある方は見てみてください。
お疲れ様です。ではさらばっ!

参考文献

https://qiita.com/howdy39/items/35729490b024ca295d6c
https://qiita.com/sho_U/items/38460b6e9bcd1dae1387
https://maeharin.hatenablog.com/entry/20130215/javascriptprototypechain
https://codezine.jp/article/detail/222
・MDN
・大岡由佳(2020.12.26)『リアクト!TypeScriptで始めるつらくないReact開発 第3.1版【Ⅰ . 言語・環境編】』pp75-82 くるみ割り書房

© 2021 powerd by UnReact