※この投稿は 2011/03/10 に こちら に投稿した記事の転載です。
これを書いた経緯
事の発端というか、きっかけは、id:perlcodesampleさんとid:gfxさんの下のポストを見て、
new
とか prototype
を使うのが推奨されてないとか、直接代入するほうが楽とかじゃなくて、挙動が違うんだよなぁ、と思ったこと。
挙動が違うんだから、もちろん使いどころも違うんですよね。
でも実際、JavaScriptのオブジェクト指向は混乱しやすいと思います。
自分もご多分にもれず、さんざん混乱させられたクチですしね。
わかってしまえば、どってことなくて、とってもシンプルなんですけどね。
せっかくなので、今だからこそ言える、自分だったらこうやって教えて欲しかったなぁ、っていう説明をしてみようかと思います。
題して、JavaScriptのオブジェクト指向は、逆から入門しろ!
逆からってどこから? → Object.createから
思うに、JavaScriptのオブジェクト指向は、new
から入るよりも、Object.create
から入ったほうが理解しやすいと思うんですよね。
Object.create
っていうのは、新しいJavaScriptの仕様(ECMAScript 5th Edition)で標準に取り入れられたメソッドで、引数で指定したオブジェクトをプロトタイプとする新しいオブジェクトを作ることができます。
こんなふうにです:
var mam = {
given_name : "サザエ",
family_name: "フグタ",
who_am_i : function(){ alert(this.family_name + this.given_name); }
};
var kid = Object.create(mam);
kid.given_name = "タラオ";
mam.who_am_i(); // フグタサザエ
kid.who_am_i(); // フグタタラオ
kidがmamのメソッドを継承していることがわかりますね。
そもそもJavaScriptのオブジェクト指向ってプロトタイプベースの継承なわけで、new
とかコンストラクタっていうのはなくてもいいんです。
この Object.create
メソッドからはじめて、従来からある new
とコンストラクタ、それからprototype
プロパティの挙動をみていこうと思います。
つまり、歴史的に逆順というわけです。
ちなみに、この Object.create
メソッドは、モダンなブラウザなら既に実装されているので、実際に実行して試すことができます。(今回自分は、Chrome 9.0で動作確認をしました。)
そもそもプロトタイプってなんなん?
上のコードを見て、「Object.create
の中では新しいオブジェクトを生成して mam
のプロパティをコピってるんだな、こんなふうに↓」
Object.create = function ( o ) {
var p = {};
for ( var i in o ) {
p[i] = o[i];
}
return p;
};
と思ったアナタ!イイ線いってます!
でも、事はそれほど単純ではありません。次のコードを続けて実行してみましょう:
mam.call = function(){ alert("カツオ!"); };
mam.call(); // カツオ!
kid.call(); // カツオ!
call
は mam
にしか追加していないのに、kid
のほうでも使えるようになってしまいましたよ。
実は mam
と kid
は同じオブジェクトを指している?いやいや、上ので who_am_i
を呼んだ時には、ちゃんと別々になってましたよね。
種明かしをすると、kid
は自身にセットされた given_name
プロパティ以外はなんにも持っていない、ほとんど空っぽのオブジェクトです。
ただし、自分が持っていないプロパティについては mam
に問い合わせて、mam
のものを自分のものとして使おうとするようになっています。
実は kid
はユーザの気づかないところで mam
への参照を隠し持っていて、自分が知らないことはmam
のマネをするわけなんですね。
このような特別な関係があるとき、mam
は kid
のプロトタイプだと言います。
また、kid
が mam
に問い合わせたプロパティを mam
も持っていなかったとしましょう。
すると mam
は、やはり自分が持っている隠し参照を使って、自分自身のプロトタイプへと問い合わせを伝搬させます。
このようにプロパティの探索をたどっていくリンクの連なりを、プロトタイプチェインと呼んだりします。
最終的に、この鎖の終端は通常は Object.prototype
になっており、終端にたどりついてもまだ該当するプロパティが見つからなかった場合に、プロパティが未定義ということになります。
Object.create
は、オブジェクト間でこのような特別な関係を築いてくれるわけですね。
オーバーライドは子供で値を設定するだけ
ところで、上の call
メソッドでは、kid
も mam
のマネをしてカツオを呼び捨てにしてしまっていました。
ここはひとつ、もう少し子供らしく呼ぶようにしつけをしてあげましょう:
kid.call = function(){ alert("カツオおにいちゃん"); };
kid.call(); // カツオおにいちゃん
プロパティ探索の挙動がわかってしまえば、挙動を変えたい場合は単に値を代入してしまえば良いことがわかりますね。
もちろん、この段階でも mam
の call
の値は元のままです。
mam.call(); // カツオ!
逆に、kid
独自の挙動を元に戻したいと思ったら、プロパティを削除してやればOKです:
delete kid.call;
kid.call(); // カツオ!
kid
からプロパティを消しても、mam
からも消えるわけではないので、ふたたびマネをするようになるわけですね。
継承とMix-in
ただ同じ振る舞いをさせたいだけであれば、単純に片方のオブジェクトからもう片方のオブジェクトへとすべてのプロパティをコピーしてしまえば良い話です。
このように既存の実装コードを、まったく関係のないオブジェクト間で再利用するというのは、継承ではなくMix-in(Rubyのincludeとか)の考え方です。
利用ケースによっては、Mix-inと継承はまったく同じような結果をもたらします。
しかしプロトタイプ継承を使うと、親への機能拡張(メソッドの追加)が、すぐさま子供からも使えるようになるという点が大きな違いです。
実行時に、プロトタイプチェインに連なっているすべてのオブジェクトについて、一度の変更で機能を追加・変更できます。
また、どれだけたくさんの子オブジェクトが作られていても、たとえ子オブジェクトにアクセスすることができなかったとしても、変更の影響を及ぼすことができます。
Mix-inとは違って、親子オブジェクト間で特別な絆ができている継承でなくては、こう柔軟にはいきませんよね。
しかし、継承がかさんでプロトタイプチェインがあまりに長くなってしまうと、ずっと親の代で定義されているプロパティを探索するのに多くの時間がかかってしまうことにもなります。(このあたりは処理系の最適化にも大きく依存しますが)
だからといって、なんでもかんでもそれぞれのオブジェクトが自分で持っていては、メモリ効率も悪くなるでしょうし、継承を使った柔軟な挙動変更もできなくなってしまいます。
結局、継承とMix-inは適材適所。
その場で相応しいほうを使い分けることができてこそ、真のJavaScripterというものですよね!
プロトタイプ関係を作れるのはnewだけ(だった)
ここからは、上で使った Object.create
の機能を、従来のJavaScriptだけで実装してみましょう。
いきなりコードで書くと、こういうことになります:
Object.create = function ( o ) {
function dummy(){}
dummy.prototype = o;
return new dummy();
}
(※実際の Object.create
は、変更不可能なプロパティを定義できたりと、3rd Editionの範囲では決して実現できなかった機能も含む高機能なものです!ここではあくまで、プロトタイプの継承関係の構築のみに焦点を当てています。)
空の関数である dummy
を new
して返す前に、dummy
の prototype
プロパティにプロトタイプとなるオブジェクトを設定しているのがミソです。
JavaScriptでは、 new F(ARGS)
という式は、次のように処理されます:
- 新しい空のオブジェクトを作る
- 新しいオブジェクトのプロトタイプとして
F.prototype
を設定する -
F(ARGS)
を呼び出す。この時、呼び出され側のthis
の値として新しく作ったオブジェクトを使う - 3の結果がオブジェクトだったら、これを結果として返す。
- そうでなければ、1で作ったオブジェクトを返す
擬似コード的に書くと、こんなかんじ:
function new ( F, ARGS ) {
var o = {};
o.__proto__ = F.prototype;
var r = F.apply(o, ARGS);
return r != null && r instanceof Object ? r : o;
}
あれ、ここで使ってる __proto__
って、どこかで見たことありませんか?
そうです。継承するコードでたまに出てくるやつですね。FirefoxとかChromeとか、特定の処理系だけで使えます。
実はこれが、上で言っていた「ユーザが気づかないところで持っている隠し参照」のことです。
処理系によっては、これがユーザから直接触れるようになっているということですね。
直接いじれちゃえば、もっと細かい制御もやり放題なので、これは便利!なのですが、もちろん、ブラウザ間の互換性が損なわれるので、あまりオススメはしません。
非標準なAPIは、ちゃんと使える場所と使いどころとをわきまえて使いましょうね。JavaScripterとの約束だよ!
newいらない子(ェ
上で new
を実装するために __proto__
プロパティを使いましたが、 Object.create
を標準で手に入れた我々は今や、__proto__
に頼らずともオブジェクトの親子関係を作れる力を手にしたのでした。
つまり、new
に相当する機能をJavaScript標準の機能だけで実装することができるようになったのです!
こんなふうに:
function New ( F, ARGS ) {
var o = Object.create(F.prototype);
var r = F.apply(o, ARGS);
return r != null && r instanceof Object ? r : o;
}
function Cat ( name ) {
this.name = name;
}
Cat.prototype.caterwaul = function(){
alert(this.name + "「ニャーニャー」");
};
var cat = New(Cat, ["タマ"]);
cat.caterwaul(); // タマ「ニャーニャー」
上では new
を使って Object.create
を実装する例を示しましたが、今回はその逆です。
つまり、Object.create
が最初からあれば、new
っていらなかったってことですね(>_<)
蛇足: なんでわざわざ関数のプロパティを経由するようになってるの?
なんででしょうね?
ここからは勝手に推測してみますが、たぶん、関数だけでなんとかしてJavaのコンストラクタの見た目をマネしたかったんじゃないでしょうか?
普通は、一連の機能をもったオブジェクトをただひとつ一点物で作る(シングルトンパターンみたいな)ケースよりも、同じ機能をもったオブジェクトをたくさん量産するケースのほうが多いと思います。
すると当然、オブジェクトの初期化を行う処理をサブルーチン化しておいて、その中でオブジェクトを生成して返すことを考えますよね。
こんなふうに:
function bear ( name ) {
var k = Object.create(mam);
k.name = name;
return k;
}
var kid = bear("タラオ");
つまるところこれって、コンストラクタですよね?
また一方で、JavaScriptはその出自から、Javaに見た目を似せなければならないという宿命を持って生まれてきました。(このあたりは、id:badatmathさんのプレゼン資料 がとってもわかりやすいです)
で、Javaってコンストラクタを呼び出すときには new
をつけますよね?
そこで、new
をつけて関数を呼び出したときに特別な処理を持たせ、関数にコンストラクタとクラスの2つの役割を押し付けてしまおうと考えたのではないかと思います。
そうなると、Javaのコンストラクタっぽく見せるためには、関数が new
をつけて呼び出されたときには、新しいオブジェクトが適切に(プロトタイプから継承されて)作られ、this
にバインドされていなければなりません。
そこで、そのコンストラクタ(関数)がオブジェクトを生成する際に暗黙的に使うプロトタイプを、コンストラクタ自身に持たせておこう。でも、どの関数がコンストラクタとして呼び出されるかはわからないから、すべての関数にプロパティで持たせておこう。
っていうことになったんじゃないかと。
えっ、JavaScriptは最初からプロトタイプベースのオブジェクト指向を採用するつもりだったのかって?
それはわかりません。Sun Microsystemsがプロトタイプベースの始祖であるSelfの研究者を抱えていたってこと以外、私は知りません。
開発者のEich氏は、最初からSchemeのような関数型言語を作るつもりだったようですけどね。
書いたあとに一言
うん!
長々と書いたわりには、全然わかりやすくなってない気がするね(´・ω・`)