Ruby脳が理解するJavaScriptのオブジェクト指向
(追記:2012-12-15) 本記事およびこれに続くその2,その3をまとめて電子書籍化しました。「Gumroad」を通して100円にて販売しています。内容についての追加・変更はありませんが、誤記の修正およびメディア向けの調整を行っています。
電子書籍「Ruby脳が理解するJavaScriptのオブジェクト指向」EPUB版
このリンクはGumroadにおける商品購入リンクになっています。クリックすると、オーバーレイ・ウインドウが立ち上がって、この場でクレジットカード決済による購入が可能です。購入にはクレジット情報およびメールアドレスの入力が必要になります。購入すると、入力したメールアドレスにコンテンツのDLリンクが送られてきます。
購入ご検討のほどよろしくお願いしますm(__)m
関連記事: 電子書籍「Ruby脳が理解するJavaScriptのオブジェクト指向」EPUB版をGumroadから出版しました!
「世の中がRubyで埋まればいいのに」と思う僕の気持ちとは裏腹に、世界は一層多様で複雑なものに向かっています。エントロピーは日々増大しています。
人々は、その競争原理を指して「多様性は善である」といいます。しかし他者の意見が理解できたとき、その多様性は失われるのです。つまり多様性とは他者に対する不理解が継続する状態を言うのです。他者を理解したときに歩み寄りのプロセスは開始され、それは統合に向かって動き出します。
僕たちはハリウッド映画を見ても、韓国ドラマを見ても、それが日本人が演じるドラマを見たときの如くに、胸を詰まらせ同じ色の涙を流すのです。そこに流れるのは人々の感情を揺さぶる共通の体系であり思想です。多様なものは何もありません。
僕の脳は完全にRuby脳です。他言語の知識は無いと言っていいです。その結果、プログラム言語の世界が極めて多様に見えています。これは極めて不健全で、争いの種を生み出す危険な状態です。あまり時間はありませんが、何とかして僕はここから抜けださなければなりません。世界平和のためにも。
大げさですか?ええ、人を呼び込むためのプロローグとは大体そんなものなのです :)
そんなわけで…
Ruby脳の僕がJavaScriptのオブジェクト指向をここ数日学んだので、今の理解を書いておきます。当然に不理解に基づく間違いが含まれています。ご指摘助かります。なお、以下のコードの実行結果はnode v0.6.14のREPLにおける出力に基づいています。
オブジェクトの生成
JavaScriptでのオブジェクトの生成は、Rubyのハッシュのような構文で行います。オブジェクトは一または複数のプロパティを持てます。プロパティとは、そのオブジェクトに紐付いたデータ(オブジェクトを含む)で、ラベルで参照できるものです。今、name
とage
というラベルで参照できるデータを持った2つのオブジェクトcharlie, earlを生成します。
var charlie = {
name: 'Charlie',
age: 12
};
var earl = {
name: 'Earl',
age: 14
};
charlie.name; // 'Charlie'
charlie.age; // 12
earl.name; // 'Earl'
earl.age; // 14
各オブジェクトのプロパティに対するアクセスは、上述のようにRubyのメソッド呼び出しのような方法で、.(ピリオド)
を使って行うことができます。
また、オブジェクトに対するプロパティの追加や変更は、変数に値を代入するが如くに極めて簡単に行えます1。各オブジェクトに、生まれた日からの日数を計算するageInDays
プロパティを追加してみます。
charlie.ageInDays = function() {
return charlie.age * 365;
};
earl.ageInDays = function() {
return earl.age * 365;
};
charlie.ageInDays(); // 4380
earl.ageInDays(); // 5110
Rubyと異なりJavaScriptにおいて関数(定義)はオブジェクトであり、このようにプロパティにセットできます2。プロパティ名を介して参照される関数は、()
(括弧)を付することで実行されます。従ってRubyと異なり()
は必須です。以下ではプロパティにセットされた関数をメソッド
と呼ぶことがあります。
未定義のプロパティの参照に対してはundefined
が返されます。
charlie.job; // undefined
プロパティ探索
しかし一方で、未定義ながら特定のプロパティに対しては所定の値が返されます。constructor
プロパティを呼んでみます。
charlie.constructor; // [Function: Object]
charlieオブジェクトのコンストラクタはObject関数であるという結果が返ってきました。
未定義のプロパティが呼べたという事実をどう解釈すればいいでしょうか。可能性の一つはオブジェクトの生成時にJavaScriptが自動でそのようなプロパティをセットしたということです。
確かめてみます。
charlie.hasOwnProperty('name'); // true
charlie.hasOwnProperty('constructor'); // false
charlie.hasOwnProperty('hasOwnProperty'); // false
hasOwnProperty
メソッドに対して、上で定義したname
はtrueを返しましたが、constructor
およびこの呼び出しメソッド自体もfalseを返しました。つまりこれらのプロパティはcharlieオブジェクトには存在しないのです。
つまりcharlieオブジェクトにはそのプロパティ探索に関して、別のオブジェクトがリンクされているのです。この別のオブジェクトは__proto__
プロパティで参照できます3。
charlie.__proto__; // {}
{}
、つまり空のオブジェクトがcharlieオブジェクトにリンクしていることが分かりました。このオブジェクトをJavaScriptではプロトタイプオブジェクトといいます。Rubyにおけるサブクラスに対するスーパークラスの呼び方のようなものですね。では、このオブジェクトが先のプロパティを持っているかを確かめてみます。
charlie.__proto__.hasOwnProperty('constructor') // true
charlie.__proto__.hasOwnProperty('hasOwnProperty') // true
trueが返ってきました。ビンゴです。
以上により、オブジェクトのプロパティが呼ばれたとき、そのオブジェクトに対象プロパティがあればそれを返すが、無い場合は__proto__プロパティにセットされたオブジェクトのプロパティを探索する。そして対象プロパティがそこにあればそれを返すということが分かりました。
ここで仮に、プロトタイプオブジェクトにも対象プロパティが見つからなかった場合はどうなるのでしょうか。これは想像が付きますよね。プロトタイプオブジェクトもcharlieオブジェクトと同種のオブジェクトですから、__proto__プロパティを持ってるはずです。よって、ここから更にその先のオブジェクトを辿るのでしょう。charlieの先の先、つまりそのプロトタイプオブジェクトの__proto__にセットされたオブジェクトを見てみましょう。
charlie.__proto__.__proto__ // null
__proto__プロパティは存在したものの、期待に反してnull
が返ってきました。つまりこの場合、プロパティ探索の旅(プロトタイプチェーン)はここで終了ということですね。
プロトタイプチェーンを使う
さて、この辺で最初のコードに戻ります。
var charlie = {
name: 'Charlie',
age: 12
};
charlie.ageInDays = function() {
return charlie.age * 365;
};
var earl = {
name: 'Earl',
age: 14
};
earl.ageInDays = function() {
return earl.age * 365;
};
charlie.name; // 'Charlie'
charlie.age; // 12
charlie.ageInDays(); // 4380
earl.name; // 'Earl'
earl.age; // 14
earl.ageInDays(); // 5110
このコードを見て、ムズムズしない人はいないでしょう。そうageInDays
メソッドがDRY原則に反しています。その結果どういった問題が生じるでしょう。
仮に、銀河の歪みによって地球の公転周期が今の3倍、つまり1年が365*3=1095日になったらどうなりますか?その場合、あなたはすべての人オブジェクトのageInDaysメソッドを1つづつ修正しなければなりません4。
先ほどのプロパティ探索の機構を利用してこの問題を解決します。つまり人の原型となるperson
オブジェクトを定義してプロトタイプチェーンに組み込むのです。
var person = {
name: 'unknown',
age: 1,
ageInDays: function() {
return person.age * 365 * 3;
}
}
person.name; // 'unknown'
person.age; // 1
person.ageInDays(); // 1095
personオブジェクトが生成できました。これをcharlie, earlの各オブジェクトのプロトタイプとなるよう、それらの__proto__
プロパティにセットして、ageInDaysを呼んでみます。
charlie.__proto__ = person;
earl.__proto__ = person;
charlie.ageInDays(); // 4380
earl.ageInDays(); // 5110
結果に変化がありません。残念ながら失敗しています。原因はなんでしょう。
そうでした、charlie,earlの各オブジェクトに直接定義したageInDaysメソッドがまだ生きていたのでした。これらを削除してもう一度呼んでみます。
delete charlie.ageInDays; // true
delete earl.ageInDays; // true
charlie.ageInDays(); // 1095
earl.ageInDays(); // 1095
数値に変化がありましたが、なんか計算がおかしいですね。原因は何でしょう。
もう一度personオブジェクトを見てみます。
var person = {
name: 'unknown',
age: 1,
ageInDays: function() {
return person.age * 365 * 3;
}
}
もう分かりました。ageInDaysでperson.ageを呼んでいたのが原因でした。ここは呼び出し元、つまりcharlieまたはearlのageが呼ばれなければいけません。
こういうときのためにJavaScriptにはthis
という便利なキーワードがあります。this
は呼び出し元のオブジェクトを差します。Rubyにおけるself
のようなものですね。
早速、this
を使ってperson.ageInDaysを書き換えます。
person.ageInDays = function() {
return this.age * 365 * 3;
};
charlie.ageInDays(); // 13140
earl.ageInDays(); // 15330
今度こそうまくいきました。
プロトタイプチェーンがどう変化したか確認してみます。
charlie.__proto__ // { name: 'unknown',
// age: 1,
// ageInDays: [Function] }
charlie.__proto__.__proto__ // {}
charlie.__proto__.__proto__.__proto__ // null
見事にpersonオブジェクトが間に差し込まれています。
オブジェクトコンストラクタ
さて、引き続きpersonを型とする別のオブジェクトを生成してみます。
var person = {
name: 'unknown',
age: 1,
ageInDays: function() {
return this.age * 365 * 3;
}
};
var zena = {
name: 'Zena',
__proto__: person
};
var rio = {
name: 'Rio',
age: 18,
__proto__: person
};
var jackie = {
name: 'Jackie',
age: 21,
__proto__: person
};
zena.name; // 'zena'
zena.age; // 1
zena.ageInDays(); // 1095
rio.name; // 'Rio'
rio.age; // 18
rio.ageInDays(); // 19710
jackie.name; // 'Jackie'
jackie.age; // 21
jackie.ageInDays(); // 22995
クラスベースのオブジェクト指向に慣れたRuby脳の僕にとって、このオブジェクト生成プロセスは面倒に感じられます。もっと簡便にオブジェクトを生成する方法はないでしょうか。
JavaScriptの関数が使えそうです。そう関数でオブジェクトのコンストラクタを作るのです。nameとageを引数にとって、これらをプロパティとしたオブジェクトを返す、そんな関数です。コンストラクタらしく、大文字から始まるPersonコンストラクタを定義します。
function Person (name, age) {
var proto = {
ageInDays: function() { return this.age * 365 * 3; }
};
var obj = { name: name, age: age };
obj.__proto__ = proto;
return obj;
};
ここでの重要なポイントは、ageInDaysプロパティを持ったプロトタイプオブジェクト(proto)を生成し、返されるオブジェクトの__proto__にこれをセットすることです。これで先のコードとほぼ同様5のオブジェクトをコンストラクタを使って生成できそうです。
やってみます。
var zena = Person('Zena', 1);
var rio = Person('Rio', 18);
var jackie = Person('Jackie', 21);
zena.name; // 'Zena'
zena.age; // 1
zena.ageInDays(); // 1095
rio.name; // 'Rio'
rio.age; // 18
rio.ageInDays(); // 19710
jackie.name; // 'Jackie'
jackie.age; // 21
jackie.ageInDays(); // 22995
いいですね。
…
と言いたいところですが、先のコンストラクタには問題があります。
今、地球に小惑星が衝突してその公転周期が更に2倍、つまり1年が365*3*2=2190日になったとします。結果PersonコンストラクタのageInDaysを再定義する必要が生じました。さてどうやってageInDaysを再定義しましょうか。ageInDaysを持ったオブジェクトはPersonコンストラクタ内のローカル変数で保持されているので、直接アクセスできません。でも、生成した特定のオブジェクト(例えばzena)の__proto__からアクセスできそうですね。やってみます。
zena.__proto__.ageInDays = function() {
return this.age * 365 * 3 * 2;
};
正しく再定義されたか確かめてみます。
zena.ageInDays(); // 2190
rio.ageInDays(); // 19710
jackie.ageInDays(); // 22995
確かにzenaの結果は倍になりましたが、他のオブジェクトの結果に変化はありません。何が問題でしょうか。もう一度Personコンストラクタの定義を見てみます。
function Person (name, age) {
var proto = {
ageInDays: function() { return this.age * 365 * 3; }
};
var obj = { name: name, age: age };
obj.__proto__ = proto;
return obj;
};
あー、ダメな理由がわかりました。
これではPersonが実行される度にプロトタイプオブジェクトprotoが作成されてしまいます。つまりPersonで生成される各オブジェクトの__proto__にはそれぞれ別のプロトタイプオブジェクトがセットされてしまうのです。
確認してみます。
rio.__proto__ == zena.__proto__ // false
zena.__proto__ == jackie.__proto__ // false
rio.__proto__ == jackie.__proto__ // false
やはり別のオブジェクトでした。
ではどうすればいいでしょうか。
そう、各オブジェクトのプロトタイプオブジェクトをPersonに紐付ければいいのです。つまりプロトタイプオブジェクトをPersonの任意のプロパティにセットし、これを参照させればいいのです。やってみます。
function Person (name, age) {
if (!Person.proto) {
Person.proto = { ageInDays: function() { return this.age * 365 * 3; } };
};
var obj = { name: name, age: age };
obj.__proto__ = Person.proto;
return obj;
};
ageInDaysメソッドを持ったオブジェクトをPerson.protoプロパティにセットし、このプロパティを各オブジェクトの__proto__にセットします。一応、Person.protoがセットされている場合はif文で無駄な処理が繰り返されないようにします。
さあもう一度オブジェクトを生成して試してみます。
var zena = Person('Zena', 1);
var rio = Person('Rio', 18);
var jackie = Person('Jackie', 21);
zena.name; // 'Zena'
zena.age; // 1
zena.ageInDays(); // 1095
rio.name; // 'Rio'
rio.age; // 18
rio.ageInDays(); // 19710
jackie.name; // 'Jackie'
jackie.age; // 21
jackie.ageInDays(); // 22995
プロトタイプオブジェクトのageInDaysメソッドを書き換えて、再度各オブジェクトから呼んでみます。
Person.proto.ageInDays = function() {
return this.age * 365 * 3 * 2;
};
zena.ageInDays(); // 2190
rio.ageInDays(); // 39420
jackie.ageInDays(); // 45990
今度はうまくいきました。
念のため各オブジェクトが共通のプロトタイプを参照しているか確認します。
rio.__proto__ == zena.__proto__ // true
zena.__proto__ == jackie.__proto__ // true
rio.__proto__ == jackie.__proto__ // true
いいですね。
new 演算子
ここまで来れば僕が何を言いたいのかが分かると思います。
「それ、new
演算子でできるよ!」ってことですね。
new演算子には関数コンストラクタを渡しますが、通常の関数の書き方でない特殊な構文の関数を構築して渡します。つまりクラスベースのオブジェクト指向におけるクラスをイミテートした構文の関数を使います。先のPerson関数と等価の関数コンストラクタは次のようになります。
function Person (name, age) {
this.name = name,
this.age = age,
};
Person.prototype.ageInDays = function() {
return this.age * 365;
}
確かにクラスっぽい。
関数コンストラクタ(これも当然オブジェクトです)には、それ専用のprototype
という名のプロパティが用意されています。既定でここには空のオブジェクトがセットされています。関数をnewすることにより、そこから生成される各オブジェクトの__proto__プロパティにはコンストラクタのprototypeプロパティがセットされます。また、コンストラクタ内のthis
ですが、これは生成される各オブジェクトを指すようになるのです。それがnew
の機能です。
さあPersonコンストラクタをnewしてオブジェクトを生成してみましょう。
var charlie = new Person('Charlie', 12);
var earl = new Person('Earl', 14);
charlie.name; // 'Charlie'
charlie.age; // 12
charlie.ageInDays(); // 4380
earl.name; // 'Earl'
earl.age; // 14
earl.ageInDays(); // 5110
Person.prototype.ageInDays = function() { return this.age * 365 * 3; };
charlie.ageInDays(); // 13140
earl.ageInDays(); // 15330
いいですね!
関数コンストラクタの注意点は、これはあくまで関数であり、newが無くても呼べてしまうということです。この場合、上記newの機能は働きません。つまり関数内部のthis
はオブジェクトを指すのではなく(オブジェクトが生成されないので当然です)、その呼び出し環境すなわちグローバルオブジェクト
を指すことになるのです。そのリスクから「newは良くない部品」と言う意見もあるようです。
「継承」「Object.create」などについての説明が欠落していますが、JavaScriptのオブジェクト指向に対する僕の理解は今のところここまでです。最後までありがとうございますm(__)m
電子書籍「Ruby脳が理解するJavaScriptのオブジェクト指向」EPUB版
このリンクはGumroadにおける商品購入リンクになっています。クリックすると、オーバーレイ・ウインドウが立ち上がって、この場でクレジットカード決済による購入が可能です。購入にはクレジット情報およびメールアドレスの入力が必要になります。購入すると、入力したメールアドレスにコンテンツのDLリンクが送られてきます。
(追記:2012-09-11) 関連記事書きました。
(追記:2012-09-15) 続きを書きました。
Ruby脳が理解するJavaScriptのオブジェクト指向(その2) __
blog comments powered by Disqus