一から勉強させてください( ̄ω ̄;)

最下級エンジニアが日々の学びをアウトプットしていくだけのブログです。

JavaScriptでクラスっぽいコードを書く方法

今回はJavaScriptでクラスっぽいコードを書く方法について考察したいと思います。

参考にさせていただいたのは、JavaScriptで関数の多重定義を行う方法の時と同様、ジョンレシグ氏の本を参考にさせていただきました!神っ!!


早速サンプルコードを。こんな感じです。
(実際にクラスと呼んでいいかはわからないですが、とりあえず以降、クラスって言葉を使わせていただきます。)

//Object.subClass作成

(function(){
    var initializing = false,
        superPattern = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
    
    Object.subClass = function(properties){
        var _super = this.prototype;

        initializing = true;
        var proto = new this();
        initializing = false;
        
        for(var name in properties){
            proto[name] = typeof properties[name] == "function" && typeof _super[name] == "function" && superPattern.test(properties[name])? 
                (function(name, fn){
                    return function(){
                        //すでにthis._superをもっているときのために一時的にtmpに格納しておく。
                        var tmp = this._super;

                        //this._superにスーパークラスの同名メソッドをセット。
                        this._super = _super[name];

                        //サブクラス内で処理を実行。この際、スーパークラスの同名メソッドもthis._superで使える。
                        var ret = fn.apply(this, arguments);

                        //this._superの値を元の値に戻しておく。
                        this._super = tmp;

                        return ret;
                    };
                })(name, properties[name]) : 
            properties[name];
        }
        
        function Class(){
            if(!initializing && this.init){
                this.init.apply(this, arguments);
            }
        }
        Class.prototype = proto;
        Class.constructor = Class;
        Class.subClass = arguments.callee;
        
        return Class;
    };
})();


//Object.subClassの使用例
//まずはクラスを作成する
var Animal = Object.subClass({
    init: function(name){
        this.name = name;
        //もしinitializingを用意しなければ単にクラスを作る処理で一回、インスタンスを作成する際に一回、
        //余計にinitが呼ばれてしまう。
        console.log("init!");
    },
    breathe: function(){
        console.log(this.name + "ふーふー");
    }
});

var Cat = Animal.subClass({
    //init, breatheはコメントアウトしてもAnimalのやつ使える。
    //this._superで参照してもOK。
    init: function(name){
        this._super(name);
    },
    breathe: function(){
        this._super();
    },
    cry: function(){
        console.log("にゃーにゃー");
    }
});

var Lion = Cat.subClass({
    init: function(name, tension){
        this._super(name);
        //Lionのみがもつプロパティ。こんな感じでthis._superでスーパークラスのメソッドを参照しつつ、拡張もでける。
        this.tension = tension;
    },
    breathe: function(){
        this._super();
    },
    cry: function(){
        //tension高いときは「がおーがおー」、高くないときはダルいから「にゃーにゃー」みたいな。
        if(this.tension == "high"){
            console.log("がおーがおー");
        }else{
            this._super();
        }
    }
});

//それぞれのクラスからインスタンスを生成する
var animal = new Animal("あにまる"),
    cat = new Cat("ねこ"),
    lion = new Lion("らいおん", "high"),
    sick_lion = new Lion("病んでるらいおん", "low");

//メソッドを実行してみる
animal.breathe(); //あにまるふーふー
cat.breathe(); //ねこふーふー
cat.cry(); //にゃーにゃー
lion.breathe(); //らいおんふーふー
lion.cry(); //がおーがおー
sick_lion.breathe(); //病んでるらいおんふーふー
lion2.cry(); //にゃーにゃー


こんな感じで、クラスっぽいコードを書く事ができました。

ポイントとしては、


・すべてのオブジェクトはObjectを継承しているので、こいつに対してsubClassメソッドを作ることで、全オブジェクトがこのメソッドを使えるようになっている。

コンストラクタの作成は各クラスにinitメソッドを仕込んでおくだけでOK。

メソッドのオーバーライドもできるし、this._superを使ってスーパークラスの同名のメソッドにアクセスもできる。


ってなところです。


ではObject.subClassの大まかな流れを追って行きたいと思います。


1、関数シリアライズがブラウザ側で対応しているかチェック

今回、関数内で_superという文字列が使われているかを調べたいので、ブラウザ側では関数シリアライズ(関数を受け取ってそのソーステキストを返す処理のこと、ほとんどのブラウザではtoString()がやってくれる)。


ここでは

/xyz/.test(function(){xyz;})


の部分でそれをチェックしています。
ブラウザ側で正しくtoStringが動作して関数がシリアライズされれば、この判定はtrueとなるので、_superにマッチする文字列があるかどうかをチェックしても問題ないということになります。


2, サブクラスを初期化する

変数_superにスーパークラスのprototype, protoにスーパークラスインスタンスを設定しています。

これはAnimalを継承するCatをつくりたいとき、

Cat.prototype = new Animal();


ってやるのと同じですね。

あと、initializingっていう変数ですが、これはinitメソッド

console.log("init!");


とか仕込んでみたらわかりやすいと思うのですが、もしinitializingの部分を完全に除外して上記のコードを実行したら、「init!」が計6回表示されます。いっぽう、initializingの分岐ありの場合は4回です。

これはインスタンス作成時の4回に、AnimalクラスからCatクラスをつくる時と、CatクラスからLionクラスをつくる時の2回分がプラスされているからですね。

ただプロトタイプを参照したいだけの時にいちいちinitを実行されるとうざいので、それを防ぐためにinitializingを切り換えてうまいこと防いでます。


3、スーパークラスメソッドを保存しつつ、プロパティ(メソッド)をprotoに登録していく。

次はメインの部分です。

ここでは


・サブクラスのプロパティは関数(つまりメソッド)か

スーパークラスのプロパティは関数(つまりメソッド)か

・サブクラスのメソッド内に_superへの参照が含まれるか(_superっていう文字がそのメソッド内に見つけられるか)


の3点をチェックして、処理を分岐させています。


上記のすべてに当てはまらない場合は、単純に

proto[name] = properties[name]


といった単純なマージになります。


一方、当てはまっちゃった場合は少々ややこしくなり、


まずtmpに古いthis._superを格納。(古いthis._superは存在しない場合もあり)

this._superに_super[name]を格納。

サブクラス内の処理を実行する。(この際、スーパークラスの同名メソッドもthis._superから参照可能)

this._superにtmpを入れ直して、もとの状態に戻してからリターン。


って感じで処理がすすみます。

これによってスーパークラスメソッドの参照もできるようになりました。


4, コンストラクタ作成用のClassを作成して返す。

最後にinitializingがfalse(つまりインスタンス生成のとき)+initメソッドを持っている場合に限り、initを実行する関数Classを用意。

こいつのprototypeにこれまで苦労してつくってきたprotoを、constructorにはこいつ自身を、subClassにはarguments.callee(呼び出し元の関数)をセットして返します。


これで後はインスタンス生成時にinitが実行されれば初期化完了です。


subClassメソッドについて簡単にまとめると、


・サブクラス=スーパークラス.subClass({サブクラスのprototypeに設定したい内容})って感じでつかう。

・サブクラス自身には先ほどみたClassが入っており、これをコンストラクタと見なす。ただし、実際の初期化内容はprototypeのinitメソッドに書いておく。

・this._super()って書いておけば、スーパークラスメソッドも使える(しかもメソッド内からしかアクセスできない)。


って感じですかね。


こんな感じでざっと追ってみましたが、地味にややこしい部分もありました。デバッグツールとか使って処理を追いまくってしまいましたw

ただかなり便利でクールなテクニックだとは思うので、今後も隙をみて使っていけたらなーって思います。


がんばろー。

小さな事からコツコツと。