JavaScriptにRuby風のnewメソッドを加える

Posted by Hiraku on 2012-05-07

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

JavaScriptの最新記事