プロトタイプチェーンをもっと理解する
Markdown が使いたくて、はてなダイアリーからはてなブログに引っ越してきました。ちょっと書いては放置してるブログの残骸があちこちに散らかっております。
前回プロトタイプについて勉強してみてふんわり分かった気になったけど、その中で 1 箇所よく分からなかったところがありました。
前回のおさらい
Javaと比較しつつ、JavaScriptのプロトタイプについて調べてみる - tacamy memo の「分からなかったところ」を参照。
前回分からなかったところ
プロトタイプには次のような特徴がありました。
- プロトタイプの中身は、そのオブジェクトを基に生成されたインスタンスから参照される (暗黙の参照)
- 同じオブジェクトを基に複数のインスタンスを生成した場合、どのインスタンスからも同じプロトタイプの中身 (プロパティ) を参照できる
- 複数のインスタンスからひとつのプロトタイプを参照しているだけなので、元のコンストラクタのプロトタイプを変更すると、インスタンスにも反映される
- インスタンスからプロトタイプのプロパティの値を変更すると、別のメモリ領域にそのインスタンス自身のプロパティとして新たに定義されるので、元のコンストラクタのプロトタイプには影響を与えない
javascript のオブジェクト指向とかプロトタイプとか - (゚∀゚)o彡 sasata299's blog の「プロトタイプの再定義とオーバーライド」の項で、7 行目でコンストラクタのプロトタイプのプロパティ値を変更しているのに、それを参照している redBox.color が blue にならないのが不思議だったのです。でも、その点については、次のように理解して解決しました。
- 7 行目は、プロパティの値を変更したのではなく、オブジェクトを再定義し直したのではないか
- 再定義することで、名前は同じ Box.prototype.color だけど、それ以前とは別のオブジェクトとなってしまったのではないか
- その根拠は、13 行目では、プロパティの値を yellow に変更しているだけなので、この場合はインスタンスである blueBox.color も yellow に変わっているから
新たな疑問
ここで、また新たな疑問が発生しました。13 行目でコンストラクタのプロトタイプのプロパティ値を変更しているのに、同じコンストラクタから生成した blueBox.color は連動して変わるのに、redBox.color は値が変わらないのはなぜだろう、と。
今回勉強したこと
変数には、基本型と参照型がある
数値や真偽値などは基本型で、オブジェクトは参照型で、それぞれ次のような違いがあります。
項目 | 基本型 | 参照型 |
---|---|---|
型の分類 | number, string, boolean, undefined, null, |
object, |
変数に格納されるもの | 値そのものを直接格納 | オブジェクト本体が保存されているメモリ上のアドレスを格納 |
変数のコピー | 値そのものがコピーされ、別々の要素ができる | アドレスがコピーがされ、オブジェクトそのものはひとつのまま |
ここで大事なのは、参照型のオブジェクトをコピーしたとき、オブジェクト自身が複製されるのではなく、オブジェクトへのアドレスがコピーされるので、実際のオブジェクトはひとつのまま増えないということです。
コンピュータのメモリ上で、それぞれ値がどのように保存されているかのイメージを図にしてみました。
JavaScript のオブジェクトのコピーは参照のコピー
オブジェクトのコピーは参照(アドレス)のコピーということは、どれだけオブジェクトをコピーしても、オブジェクトの実体はひとつだけということです。
ということは、コピーしたオブジェクトの片方の値を変更すると、もう片方も連動して変わります。
参照しているオブジェクトの実体は同じものなので、当然の結果です。
次のコードを使って、具体的に理解を深めます。
// sample1 var foo = { color: 'red' }; // foo オブジェクトを生成 var bar = foo; // foo の実体への参照を bar にコピー bar.color = 'blue'; // foo オブジェクトの color プロパティの値を変更 console.log(foo.color); // blue console.log(bar.color); // blue // sample2 var foo = { color: 'red' }; // foo オブジェクトを生成 var bar = foo; // foo の実体への参照を bar にコピー bar = { color: 'blue' }; // 新しい別のオブジェクトを定義 console.log(foo.color); // red console.log(bar.color); // blue
sample1 と sample2 のどちらも、var bar = foo;
の時点では、foo と bar どちらも同じ foo オブジェクトへの参照が格納されています。
sample1 の bar.color = 'blue';
は、参照先の foo オブジェクトが持っている color プロパティの値を変更しました。
foo と bar はどちらも同じ foo オブジェクトへの参照なので、そのオブジェクト実体のプロパティの値が変更されれば、連動して値が変わります。
これは、今までの流れに沿って考えると、違和感なく受け入れられます。
次に、sample2 の bar = { color: 'blue' };
についてです。
bar に新しく別のオブジェクトを定義し直しているので、bar に格納されていた foo オブジェクト実体への参照は、別のオブジェクトへの参照に置き換わってしまいました。
この時点で foo と bar は同じオブジェクトの参照ではなく、別々のオブジェクトの参照となってしまったため、bar の変更が foo と連動しなくなりました。
よって、foo.color と bar.color の結果も連動しません。
prototype と __proto__ の違い
プロトタイプチェーンについてもう一度おさらいします。
function Super() {} Super.prototype.a = 10; var Sub = function() {} Sub.prototype = new Super(); var instance = new Sub(); console.log(instance.a); // 10
上記のコードでは、次のことをしています。
- スーパークラス Super のプロトタイプに a プロパティを定義
- サブクラス Sub のプロトタイプで Super のインスタンスを格納
- 変数 instance に Sub のインスタンスを作成して格納
instance から暗黙の参照によって、Super のプロトタイプにある a プロパティの値を表示する流れは次のとおりです。
- instance 自身に a プロパティはあるか探す
- 見つからないので、Sub の prototype を探す
- 見つからないので、Super の prototype を探す
- 見つかったのでここで Super.prototype.a の値を返す
ところでこの暗黙の参照、どうやって実現しているかよく考えてみると疑問です。自分の継承元の prototype をどうやって探してるのかなと。
ここで __proto__ プロパティの出番です。__proto__ プロパティには、次のような特徴があります。
- __proto__ はすべてのオブジェクトが持つプロパティ
- オブジェクトに対象のプロパティが見つからなければ、__proto__ の指すオブジェクトを探す
- __proto__ の値が null になったらそこで探索終了
- Object.prototype (すべてのオブジェクトの元) の __proto__ には null が入っている
- new したときに、コンストラクタの prototype への参照(アドレス)を格納する
親の prototype がある場所(アドレス)を、自動でそれぞれのオブジェクトの __proto__ に代入してくれているイメージです。次のコードで __proto__ へのアドレスの代入のイメージを書いてみましたが、実際にこれを自分で書く必要はありません。
function Super() {} Super.prototype.a = 10; Super.prototype.__proto__ = Object.prototype; // Object.prototype のアドレス情報 var Sub = function() {} Sub.prototype = new Super(); Sub.prototype.__proto__ = Super.prototype; // Super.prototype のアドレス情報 var instance = new Sub(); instance.__proto__ = Sub.prototype; // Sub.prototype のアドレス情報
この __proto__ が持っているアドレス情報をどんどん遡っていくことによって、プロトタイプチェーンが実現できます。
「新たな疑問」を振り返る
ここまで勉強したので、javascript のオブジェクト指向とかプロトタイプとか - (゚∀゚)o彡 sasata299's blog の「プロトタイプの再定義とオーバーライド」の項をもう一度見てみます。
13 行目でコンストラクタのプロトタイプのプロパティ値を変更しているのに、同じコンストラクタから生成した blueBox.color は連動して変わるのに、redBox.color は値が変わらない理由を考えてみます。
- 4 行目で
var redBox = new Box();
をした時点では、redBox.__proto__ には、Box.prototype のアドレス情報が入る(仮に1000番地とする) - 7 行目の
Box.prototype = { color: "blue" };
でオブジェクトが再定義されたため、Box.prototype の値(参照)が、別のオブジェクトへの参照に変わってしまった(仮に2000番地とする) - この時点で、redBox.__proto__ は、Box.prototype の番地を指していないことになる(1000番地のまま)
- 9 行目の
var blueBox = new Box();
によって、blueBox.__proto__ に Box.prototype のアドレス情報が入る(2000番地)
この時点でそれぞれ、次の番地となっています。
- Box.prototype => 2000番地
- blueBox.__proto__ => 2000番地
- redBox.__proto__ => 1000番地
redBox.__proto__ は Box.prototype のプロトタイプチェーンから外れてしまいました。 そのため、redBox から Box のプロトタイプを見に行くことができないため、コンストラクタのプロトタイプの変更が反映されなくなってしまったのです。
やっと意味が分かった!!!長かった・・。
プロトタイプチェーンでの継承の意義
プロトタイプチェーンによって、スーパークラス(親)からサブクラス(子)へ、プロパティやメソッドを継承できます。
こういう例を出すと実務とかけ離れるので、あんまりよくないかもですが、
動物(親) > 犬(親 / 子) > チワワ(子) > ゆに(インスタンス)
みたいなイメージでしょうか。
継承には、次のようなメリットがあります。
- コードの再利用
- 新しいオブジェクトの定義にかかる手間を削減できる
- 継承しなければ、すべてのプロパティを一から定義し直さなければならない
- 継承関係によって複数のオブジェクトをグループ化できる
- プロパティの追加やメソッドのオーバーライドで機能を拡張
- メモリの節約
- 共通部分はひとつのプロパティを見に行くので、メモリ使用量を節約できる
大規模開発でこそ威力を発揮してくれそうですし、設計力が問われそうですね・・(ヽ'ω`)
まとめ
プロトタイプチェーンを使うと、JavaScript でも、Java のクラスベースのオブジェクト指向的なことができるっぽい。 でも Java とは考え方も書き方も違うところがいっぱいあるから気をつけましょう。