prototypeプロパティがよくわかるRGM-79最強伝説[オレ得JavaScriptメモ]

JAVASCRIPT

オレ得JavaScriptメモ6枚目です。こないだのメモで4枚目って言ったけどありゃウソです。間違い。

データ型の話の後、オブジェクト・Object・object型の話、thisの話をしてずいぶん遠回りしてしまったがようやくprototypeのことをまとめます。

this辺りからようやくJavaScriptらしい話になってきて良かった。きちんと整理して覚えないとまたGomiScriptになってしまうからな。

タイトルで謳ってるジムはちょっとしか出てきません、すみません。

プロトタイプとprototype

ここにfunction型オブジェクトabcとobject型オブジェクトxyzを用意する。

(データ型についてはこの辺参照。)


function abc(){}
var xyz = {};

用意した。ところでabcのデータ型は本当にfunction型?xyzのデータ型は本当にobject型?


function abc(){}
var xyz = {};
console.log(typeof(abc));//function
console.log(typeof(xyz));//object

間違いないね。確認するなんて馬鹿げてる?いやいや、念のために確認するのは大事なことだよ。

ところでabcの中には何のステートメントも書かれていない。空っぽだ。xyzの中にも何もセットしていない。空っぽだ。

空っぽだからどちらもプロパティやメソッドは持たないはずだ。持たないはずなんだが、見たまえ、これを。


function abc(){}
var xyz = {};
console.log(abc.prototype);//Object {}
console.log(xyz.prototype);//undefined

abcに作った覚えのないprototypeプロパティが勝手に追加されてるやんけなんやワレボケカス氏ね!というのは

typeof()でオーラロードをこじ開ける(もしくはデータ型について)[オレ得JavaScriptメモ] | 56docブログ

で述べた通りだ。いやボケカス氏ねとは言ってない。よいこはそんなこと言っちゃダメだよ。

一方、object型オブジェクトxyzにはprototypeなどというプロパティは存在していない。これは


function Abc(){}
var xyz = new Abc;
console.log(Abc.prototype);//Object {}
console.log(xyz.prototype);//undefined

かようにxyzをabcのインスタンスとして生成した場合でも同様だ。

結論を先に書いておく。prototypeプロパティとは、function型オブジェクトに対してのみ自動生成される特殊なプロパティだ。object型オブジェクトには存在しない。

さて、この特殊なプロパティ、prototypeはいったい何のためあるのか。

次のようなコードがどのような挙動をするのか見てみる。


function Abc(){}
Abc.prototype.p = 56;
var xyz = new Abc;
console.log(xyz.p);//56

インスタンスxyzはいかなるプロパティもメソッドもセットしていない。にもかかわらずxyz.pというないはずのプロパティを尋ねると56が返ってくる。

これはコンストラクタAbcのprototypeプロパティにさらにpというプロパティを追加したことによるものだ。この挙動を誤解を恐れず(誤解を恐れてる)に乱暴に言うと

「インスタンスxyzにpというプロパティについて聞いたら、そんなのないから代わりにコンストラクタのprototypeプロパティpを教えてやったお\(^o^)/!」

ということだ。つまりprototypeプロパティというのは、この例では「インスタンス自身が持たないプロパティを、持ってるふりをするためにコンストラクタ側で用意しておくプロパティ」ととして機能している、ということだ。

この「インスタンス自身が持たない」というのがけっこう重要だ。


function Abc(){}
Abc.prototype.p = 56;
var xyz = new Abc;
xyz.p = 78;
console.log(xyz.p);//78

ほら、インスタンス自身が自分のプロパティとしてpを持っているときはちゃんと自分のプロパティを答えるんだ。

また1つのコンストラクタから複数のインスタンスを生成してもprototypeはやっぱり同じ様に機能する。


function Abc(){}
Abc.prototype.p = 56;
var xyz = new Abc;
console.log(xyz.p);//56
var lmn = new Abc;
lmn.p = 78;
console.log(lmn.p);//78
console.log(xyz.p);//56

prototypeにあるプロパティと同名のプロパティをインスタンスのほうに追加したからと言って、prototype側にあるプロパティは影響を受けない。

それと、もう一つ大事なこと。


function Abc(){}
var xyz = new Abc;
Abc.prototype.p = 56;
console.log(xyz.p);//56

インスタンスを生成した後からコンストラクタのprotototypeプロパティにプロパティやメソッドを追加しても、その変更がインスタンスに反映される、ということ。prototypeによる代替応答は事後追加が可能なのだ。

このようなprototypeによる代替応答は「コンストラクタはインスタンスのプロトタイプ(原型)である」と言うことができる。こう言えばprototypeとプロトタイプの違いがわかるよね。

RX-78ガンダムはRGM-79ジムのプロトタイプだ。JavaScriptが機動戦士ガンダムと違うのは「RX-78のprototypeにマグネットコーティングを施すって書いたらRGM-79にも同時にマグネットコーティングが施される」ことだ。

あと「RGM-79は初めっからRX-78の能力を引き継ぐことができる」のでRGM-79が右手に持ってるやつはビームスプレーガンじゃなくてビームライフルだし「RGM-79に独自の能力も持たせられる」からビームライフルの代わりにハイパーメガランチャー持つこともできる。


function Rx78(){}
Rx78.prototype.gun = 'ビームライフル';
var rgm79 = new Rx78;
console.log(rgm79.gun == 'ビームスプレーガン');//false
console.log(rgm79.gun == 'ビームライフル');//true
rgm79.gun = 'ハイパーメガランチャー'
console.log(rgm79.gun);//ハイパーメガランチャー
Rx78.prototype.joint = 'マグネットコーティング';
console.log(rgm79.joint);//マグネットコーティング

長くなるので例には書いてないけど、やろうと思えばジムを可変MSにもできるしなんだったら全身にサイコフレームを組み込むことだってできる。JavaScriptのジムは最強だ。

さてガンダムの話はこれくらいにして、最初のほうの例をもう一度見てみよう。


function Abc(){}
Abc.prototype.p = 56;
var xyz = new Abc;
console.log(xyz.p);//56

コンストラクタのプロパティやメソッドをインスタンスに引き継ぐだけなら、わざわざprototypeを使わずにコンストラクタ内でthisで指定しても同じことができたはずだ。

thisがよくわからないのthis[オレ得JavaScriptメモ] | 56docブログ


function Abc(){
this.p = 56;
}
var xyz = new Abc;
console.log(xyz.p);//56

できるね。しかしthisを使う場合とprototypeを使う場合では決定的に違うことがある。それは「インスタンスがコンストラクタを参照するとき、this(をなんと解釈すればいいのか)はインスタンスごとに作られる」ということ。

このことは同一のコンストラクタから複数のインスタンスを作ってみればわかる。


function Abc(){
this.p = 56;
}
var xyz = new Abc;
var lmn = new Abc;
console.log(xyz.p);//56
console.log(xyz);//Abc {p: 56}
console.log(lmn.p);//56
console.log(lmn);//Abc {p: 56}

xyzをnewするときthisはxyzだ。lmnをnewするときthisはlmnだ。そしてそれぞれについて同じpというプロパティを設定している。これは大変不合理だ。同じ構造の同じ値を保持するのにそれぞれ別にメモリ領域を確保してるんだから。

インスタンスが2つや3つなら大したことではないかもしれないけれど、50個、100個になれば話は別だ。とんでもないメモリの無駄遣いだというのがわかるでしょ?

それに、これだと「インスタンスをいっぱい作った後、pをまとめて変化させる」みたいなことはこのままではできなさそうだ。これがprototypeを使えば


function Abc(){}
Abc.prototype.p = 56;
var xyz = new Abc;
var lmn = new Abc;
console.log(xyz.p);//56
console.log(xyz);//Abc {}
console.log(lmn.p);//56
console.log(lmn);//Abc {}
Abc.prototype.p = 78;
console.log(xyz.p);//78
console.log(lmn.p);//78

xyzもlmnもAbcへの単なる参照でそれぞれのpは保持しないし、各自のプロパティがなければコンストラクタのprototypeプロパティから黙って同じプロパティを拾ってくる。コンストラクタのprototype以下のプロパティを変更させればどのインスタンスにも反映できる。


function Abc(){}
Abc.prototype.p = 56;
var xyz = new Abc;
var lmn = new Abc;
console.log(xyz.p);//56
console.log(lmn.p);//56
Abc.prototype.p = 78;
console.log(xyz.p);//78
console.log(lmn.p);//78
xyz.p = 34;
console.log(xyz.p);//34
console.log(lmn.p);//78
delete xyz.p;
console.log(xyz.p);//78
console.log(lmn.p);//78

個別にプロパティが必要になったらその時各々に設定すればいいだけのことだ。delete演算子で個別のプロパティを削除すれば、prototypeへの参照に戻すのだって自由自在。

prototypeのprototype

あともう一つ、prototypeの重要な機能。「prototypeは遡れる」だ。遡れる、と言っても時間的な意味ではなく、オブジェクト構成として、ね。


function Abc(){}
Abc.prototype.p = 56;
var abcInstance = new Abc;
function Xyz(){}
Xyz.prototype = abcInstance;
Xyz.prototype.r = 78;
var xyzInstance = new Xyz;
console.log(xyzInstance.r);//78
console.log(xyzInstance.p);//56

やや長いが落ち着いて考えればわかる。

xyzInstance.r

インスタンスxyzInstanceにはrというプロパティはない。なのでそのコンストラクタのprototypeを見る。

そこにrがあって値が78になっている。だから78と応答する。これは今まで見てきた例の通りだ。

xyzInstance.p

インスタンスxyzInstanceにはpというプロパティはない。なのでそのコンストラクタのprototypeを見る。

そこにもpはない。しかしXyzのprototypeはabcInstanceだと言っている。

なのでabcInstanceを見てみると、これはどうやらAbcのインスタンスのようだ。だったらそのコンストラクタであるAbcのprototypeを見てみればわかるんじゃないか?

Abc.prototypeにpプロパティは…あった。56だ。xyzInstance.pはprototypeの中のprototypeを追っかけて56になった!

これがどうやらかの有名な「プロトタイプチェーン」というやつではないかな?ちなみに上のコードはabcInstanceを介さないでも


function Abc(){}
Abc.prototype.p = 56;
function Xyz(){}
Xyz.prototype = new Abc;
Xyz.prototype.r = 78;
var xyzInstance = new Xyz;
console.log(xyzInstance.r);//78
console.log(xyzInstance.p);//56

prototypeの中で直接newしても同様のことができちゃう。っていうかこっちの方が直観的にわかり易いかも。

「プロトタイプチェーン」というのは結局「オブジェクト自身のプロパティが見つからないとき、そのプロトタイプのprototypeから同じプロパティを捜し、なければさらにそのプロトタイプのprototypeから…」という繰り返しのことを言っているのだね。

ちなみに上の例ではXyzのprototypeそのものをAbcのインスタンスとして、そのあとにそのprototypeにpというプロパティを追加している。これが


function Abc(){}
Abc.prototype.p = 56;
function Xyz(){}
Xyz.prototype.r = 78;
Xyz.prototype = new Abc;
var xyzInstance = new Xyz;
console.log(xyzInstance.r);//undefined
console.log(xyzInstance.p);//56

このように順序が逆転してしまうと、Xyzのprototypeがまるっと置き換えられてAbcのインスタンスを参照することになり、それ以前にprototypeに追加したrは破棄されてしまうので注意が必要。代入しなおしてるんだから当たり前と言えば当たり前なんだけど。

ここまではプロパティについて考えてきたけど、チェーンで辿れるのはメソッドでも同じ。一応一つだけ例示しておく。


function shout(){
console.log('金をくれ!');
}
function Abc(){}
Abc.prototype.myshout = shout;
function Xyz(){}
Xyz.prototype = new Abc;
function Lmn (){}
Lmn.prototype = new Xyz;
var lmnInstance = new Lmn;
lmnInstance.myshout();//金をくれ!

お金下さい。

プロトタイプチェーンはどこまで遡れるか

さてプロトタイプチェーンがprototypeを次々参照していってもやっぱりそのプロパティが無いとどうなる?


function Abc(){}
function Xyz(){}
Xyz.prototype = new Abc;
var xyzInstance = new Xyz;
console.log(xyzInstance.p);//undefined

無いんだから当然無いって言われる。これは「プロトタイプチェーンがコンストラクタAbcまで遡った結果」なのだろうか?ノーだ。


Object.prototype.p = 56;
function Abc(){}
function Xyz(){}
Xyz.prototype = new Abc;
var xyzInstance = new Xyz;
console.log(xyzInstance.p);//56

プロトタイプチェーンはObjectオブジェクトまで遡るの。JavaScriptのオブジェクトはすべてObjectオブジェクトを元に作られているということを思い出そう。

Objectオブジェクト自身はfunction型オブジェクトだ。つまり関数。そしてその動作は「空のobject型オブジェクトをつくれ」というものだ。

Objectオブジェクトは関数なんだからコンストラクタになり得るし、prototypeプロパティも持っている。そしてどんなオブジェクトもObjectの派生なんだからprototypeを遡れば最終的にObjectオブジェクトにたどり着くってわけ。


function Abc(){}

っていう1行コードはAbcを定義しているだけで一見何もしてないように見えるけど


function Abc(){}
Abc.prototype = new Object;//って書いてないけど自動的にやっている

こう考えればスッキリするし、function型オブジェクトが常にObjectオブジェクトのprototypeとの繋がりを持っているというのも理解しやすいと思う。。

ところでprototypeプロパティというのは自動生成れたときには空っぽ…ではない。いくつかの基本的な(function型オブジェクトである)メソッドが用意されている。そのなかに「isPrototypeOf()」というメソッドがある。

どういうものか簡単に言うと「○○は××のプロトタイプかどうか」を返す関数だ。○○が××のプロトタイプならtrueを返し、そうでないならfalseを返す。

ここでいう「プロトタイプかどうか」は直接のプロトタイプである場合もあるし、いくつかのチェーンを介してプロトタイプである場合も考えられるが、isPrototypeOf()はそのいずれの場合もtrueを返す。

もちろん○○はfunction型オブジェクトだし、××はobject型オブジェクトね。例えば


function Abc(){}
function Xyz(){}
Xyz.prototype = new Abc;
function Lmn(){}
Lmn.prototype = new Xyz;
var lmnInstance = new Lmn;
console.log(Xyz.prototype.isPrototypeOf(lmnInstance));//true
console.log(Abc.prototype.isPrototypeOf(lmnInstance));//true

これはどちらもtrueになるね。XyzはlmnInstanceの直接のプロトタイプであるのは明らかだ。AbcもXyzを介してlmnInstanceのプロトタイプになっている。

家系図でつながりを持っているかどうかを調べる感じ、どこかでつながってればtrueを返すというイメージに近いかもしれない。


function Abc(){}
function Xyz(){}
Xyz.prototype = new Abc;
var xyzInstance = new Xyz;
var lmn = {};
console.log(Abc.prototype.isPrototypeOf(lmn));//false

コンストラクタ-new-インスタンスの流れとは全く関係なくlmnが作られた場合、Abcはlmnのプロトタイプにはなってないね。lmnはAbc~xyzInstanceの家系とは赤の他人だということがわかる。

じゃあそれぞれのご先祖様をずっと辿っていくとObjectオブジェクトにたどり着くの?


function Abc(){}
function Xyz(){}
Xyz.prototype = new Abc;
var xyzInstance = new Xyz;
var lmn = {};
console.log(Object.prototype.isPrototypeOf(lmn));//true
console.log(typeof(lmn));//object
console.log(Object.prototype.isPrototypeOf(Abc));//true
console.log(typeof(Abc));//function
console.log(Object.prototype.isPrototypeOf(new Abc));//true
console.log(typeof(new Abc));//object
console.log(Object.prototype.isPrototypeOf(Xyz));//true
console.log(typeof(Xyz));//function
console.log(Object.prototype.isPrototypeOf(xyzInstance));//true
console.log(typeof(xyzInstance));//object

バカげたコードだが、これでfunction型オブジェクトもobject型オブジェクトもみんなObjectオブジェクトとどっかで繋がってるってのがわかったね。

あ、解ってると思うけどプリミティブ型のデータは別よ。


var x = 56;
var y = '五十六';
var z = true;
console.log(Object.prototype.isPrototypeOf(x));//false
console.log(typeof(x));//number
console.log(Object.prototype.isPrototypeOf(y));//false
console.log(typeof(y));//string
console.log(Object.prototype.isPrototypeOf(z));//false
console.log(typeof(z));//boolean

単純なnumber型、string型、boolean型のデータはいずれもObjectオブジェクトの子孫ではない。

それと、これは完全に余談だけども次のコードが実に面白い。


console.log(Object.prototype.isPrototypeOf(Object));//true

ObjectオブジェクトはObjectオブジェクト自身をご先祖様にも持っているの。プロトタイプチェーンはどんどんご先祖様を辿ってObjectオブジェクトのprototypeプロパティに行きつくじゃん?

だからObject.prototypeに直接プロパティを追加しちゃうと


Object.prototype.p = 56;
var abc = {};
console.log(abc.p);//56

まぁこれは普通にチェーン辿って56返すんだけどさ


Object.prototype.p = 56;
var abc = {};
console.log(abc.p);//56
console.log(abc.p.p);//56
console.log(abc.p.p.p);//56
console.log(abc.p.p.p.p);//56
console.log(abc.p.p.p.p.p);//56
//以下想像にお任せ

こういう一見不思議なことも起こるわけ。これはお遊びでやってるので真似しないように。

まとめ

prototypeプロパティとは「オブジェクトAがあるプロパティ(またはメソッド)を持たないときに、そのオブジェクト生成の元となったオブジェクトBが持っているであろう、代替応答用のプロパティ(またはメソッド)を格納する特殊な」プロパティ。

プロトタイプとは上のような場合に「BはAのプロトタイプである」という概念。

プロトタイプチェーンとは「AにないプロパティやメソッドはそのプロトタイプであるBのprototypeプロパティを探しに行って、それでもなければそのプロトタイプであるCのprototypeプロパティを探しに行って…」というprototypeプロパティを通じたオブジェクト同士の繋がりのさま。

プロトタイプチェーンを辿っていくとどんなオブジェクトもどこかでObject.prototypeにたどり着く。

素晴らしい、まとまった。まとめらしいまとめだ。ごろどくさん天才なんじゃないだろうか。ウソです。

識者はこのまとめを仕様通りの正しい説明ではない、と多分言うと思う。が、そのためには__proto__と[[prototype]]についても考えなきゃならなくなるはずなので、今日はここまで。寒くなってきたのでとりあえずお風呂入るよ。んじゃまたー。

コメント

タイトルとURLをコピーしました