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だと色々できて楽しいですね。
keyword: javascript