JavaScriptのオブジェクト指向はクラスベースの皮をかぶったプロトタイプベースです。機能的には十分なのですが、すっきり書く方法が公式に用意されていないので苦労します。一年前に、newを封印してJavaScriptでオブジェクト指向するなんて記事を書いたこともありました。
Rubyではnewは演算子でなくメソッドです。これをインスパイヤしてJavaScriptもnewメソッドを加えてみると、プロトタイプ的継承もすっきり書けるのではないかと思い、試してみました。ECMAScript 5の機能を使っています。当然IE6なんかでは動かないです。
newメソッドその他の定義
Object.defineProperties(Object.prototype, {
new: {value: function(){
var self = Object.create(this);
self.initialize.apply(self, arguments);
return self;
}},
initialize: {value: function(){}},
instanceof: {value: function(Class){
function f(){}
f.prototype = Class;
return this instanceof f;
}},
extends: {value: function(){
var obj, i, prop, l;
obj = Object.create(this);
for (i=0,l=arguments.length; i<l; i++)
for (prop in arguments[i])
obj[prop] = arguments[i][prop];
return obj;
}},
});
Object.defineProperty(Function.prototype, "new", {value:void 0});
基本的な使い方
上記のようにObject.prototypeを拡張してあれば、ありとあらゆるオブジェクトにnew()メソッドが生えます。オブジェクトをそのままnew()できてクラス(もどき)の定義も簡単に。
/**
* Birdクラス
*/
var Bird = {
fly: function(){ console.log("ぱたぱた"); }
};
/* インスタンス化 */
var b = Bird.new();
b.fly(); //ぱたぱた
やっていることはプロトタイプ的継承そのものなのですが、Object.create()を直接使ったり、object()関数を定義したりするのに比べて読みやすいように思います。ちなみに、newの()は省略できないので注意です。
コンストラクタ
initialize()というメソッドを定義しておけば、new()の際に自動的に呼ばれるようにしました。いわゆるコンストラクタです。引数はnew()に渡したものがそのまま渡ります。なお、initialize()の戻り値は見ていないので何もreturnしなくてよいです。
var Bird = {
initialize: function(name) {
this.name = name; //インスタンス変数にセット
},
fly: function() {
console.log(this.name + "はぱたぱた飛ぶよ"); //インスタンス変数を引ける
}
};
var popo = Bird.new("ぽっぽー");
popo.fly(); //ぽっぽーはぱたぱた飛ぶよ
継承、多重継承など
extends()を使うと簡単にオブジェクトを継承できます。プロトタイプベースでは継承とインスタンス化が同義ですので、new()とやっていることの本質は同じです。ただ、initializeを呼ばなかったり、メソッドを上書きするための機構があるなどの違いがあります。
extendsとしましたが、語順が「class クラス名 extends 親クラス名 { ... }」ではなく「var クラス名 = 親クラス名.extends({ ... })」なのでちょっと変かもしれません。(いい単語はないでしょうか?)
//基底クラス Bird
var Bird = {
initialize: function(name) { this.name = name },
fly: function() { console.log(this.name + "はぱたぱた飛ぶよ") }
};
//泳ぐ能力のmixin
var SwimSkill = {
swim: function() { console.log(this.name + "はすいすい泳ぐよ") }
};
//Birdを継承してSwimSkillをmixin、さらにflyメソッドを上書きする
//多重継承させたいオブジェクトがあれば引数に追加(可変長引数)
//後ろに書いた方が上書きしていく
var Penguin = Bird.extends(SwimSkill, {
fly: function() {
//親クラスのメソッドを使いたい場合はcallやapplyを利用
//Bird.fly.call(this);
console.log(this.name + "は飛べないんだ…");
}
});
var pen = Penguin.new("ぺんぎんさん");
pen.swim(); //ぺんぎんさんはすいすい泳ぐよ
pen.fly(); //ぺんぎんさんは飛べないんだ…
initializeメソッドは何も上書きしなければ親のものがそのまま使われます。
その他
- JS本来の文法ではないのでinstanceof演算子は使えないが、代わりにinstanceof()メソッドを実装しておいた。
pen.instanceof(Penguin) === trueなど。語順も同じだし読みやすいのではないかと。
- Object.prototypeを拡張する際はObject.definePropertyもしくはObject.definePropertiesなどを使い、for〜inループに影響を与えないよう配慮すること。(enumerableをfalseにするべし)
- newやinstanceofといった予約語はプロパティラベルとして使える。(ECMAScript 5thからだっけ?)
- 組み込みのクラスやコンストラクタも頑張ればnew()メソッドに対応できそうな気がするけど、面倒くさそうなので実装していない。とりあえず既存のクラスは相変わらず
new Dateなどで。(間違って呼ばないようFunction.prototypeからはnewを削除してある。)
- この方法だとクラスメソッドは定義できない。(全部インスタンスへ継承されてしまう)
- コンストラクタの自動継承はJS組み込みのオブジェクト指向記法だとできないため、newメソッド方式の利点と言えるかもしれない。
ECMAScript 5だと色々できて楽しいですね。