自分用class定義ライブラリmyclass.jsを作ってみる(1)

自分の中でデファクトとすべく書き方を考えてみました。
いろいろと発見もあったので、はてな界隈の方々にはいまさらかとは思いますが、まとめてみました。

まずは単純に

汎用ルーチンとかに頼らず書いてみる。

/* 親 */
var Car = function(){}
Car.prototype = {
    voice : 'boo',
    tire : 4,
    init : function(p){
        if(p){
            this.tire = p.tire;
        }
        return this;
    },
    showSpec : function(){
        alert('voice is ' + this.voice)
        alert('tire is ' + this.tire)
        return this;
    }
}
/* 子 */
var DumpCar = function(){}
DumpCar.prototype = new Car();  /* 継承 */
DumpCar.prototype.voice = 'GAAAA!';
DumpCar.prototype.hasBed = 'yes';
DumpCar.prototype.init = function(p){
    Car.prototype.init.apply(this,arguments)
    return this;
}
/* 実行 */
( (new DumpCar()).init({tire:6}).showSpec())

プロパティ定義方法の親と子の違い

親はprototypeの参照先を書き換えてしまう方法で、子はDumpCar.prototype.xxx にいちいち値やメソッドをつっこむ方法で定義してます。
親のような記述の方が見やすいわけですが、子でそれができないのは、

DumpCar.prototype = new Car();  /* 継承 */
DumpCar.prototype = {
    voice : 'GAAAA!',
    hasBed : 'yes';
    init : function(p){
    ...
}

↑このような記述は、

DumpCar.prototype = new Car();  /* 継承 */
DumpCar.prototype = new Object; // あるいは DumpCar.prototype ={}
DumpCar.prototype.voice = 'GAAAA!';
DumpCar.prototype.hasBed = 'yes';
DumpCar.prototype.init = function(p){
...

↑これと同じ事を意味するので、new Car()で生成されたobjectがprototypeプロパティから参照できなくなってしまうため。

解決方法1
prototype.jsのextendような汎用ルーチンを使うと見通しの良い記述ができます。

var extend = function(target,obj){
    for(var i in obj){
        target[i]=obj[i];
    }
}
...
/* 子 */
var DumpCar = function(){}
DumpCar.prototype = new Car();  /* 継承 */
extend(DumpCar.prototype,{
    voice : 'GAAAA!',
    hasBed : 'yes';
    init : function(p){
    ...
});

解決方法2
あるいは無名関数を使って以下のようにもできます。

var DumpCar = (function(f,ext){
    var c = (f.prototype = new ext);
    c.voice = 'GAAAA!';
    c.hasBed = 'yes';
    c.init = function(p){
        ...
    }
    return f;
})(function(){},Car)    //親の定義でも使用する場合は、})(function(){},Object)とすればOK

インスタンス化とコンストラクタの実行

new で実体化した後、initで初期化処理を行うという冗長な記述になってます。

/* 実行 */
((new DumpCar()).init({tire:6}).showSpec())

本来、以下の書き方で済むようにしたいところです。

((new DumpCar({tire:6})).showSpec())

これは、コンストラクタ内( var Car = function(){/*ここ*/} ) に、init と同様の処理を記述するか、
init をthis.init.apply でコールすれば実現できますが、親の継承のnewでも親のinitが実行されてしまうという問題が発生してしまいます。

/* 親 */
var Car = function(){this.init.apply(this,arguments)}

                                                • -
/* 子 */ var DumpCar = function(){this.init.apply(this,arguments)} /* 継承でCar.prototype.initが実行されてしまう */ DumpCar.prototype = new Car();
                                                • -
/* 孫での継承でもDumpCar.prototype.initが実行されてしまう */ TuckYarou.prototype = new DumpCar();
解決方法1

継承で new を使うためにinit が実行されてしまうので、前述のextendような処理で継承をすればこれを抑止できます。

var extend = function(target,obj){
    for(var i in obj){
        target[i]=obj[i];
    }
}
...
/* 子 */
var DumpCar = function(){this.init.apply(this,arguments)}
extend(DumpCar.prototype,Car.prototype);   /* 継承 */
extend(DumpCar.prototype,{
    voice : 'GAAAA!',
    hasBed : 'yes';
    init : function(p){
    ...
});

但しこの場合は、prototypeプロパティのobjectの書き換えを行ってないため、
インスタンス化されたobjectのconstructorプロパティのobjectが変わっていません。
必要に応じてconstractorの書き換えを行います。

/* new で継承した場合 */
DumpCar.prototype = new Car();/* 継承 */
...
alert((new DumpCar).constructor.prototype.tire) //4
/* extend関数 で継承した場合 */
extend(DumpCar.prototype,Car.prototype);/* 継承 */
...
alert((new DumpCar).constructor.prototype.tire) //6

/* constructorの書き換え */
DumpCar.prototype.constructor=Car
alert((new DumpCar).constructor.prototype.tire) //4
解決方法2

他の空functionを間借りすることで解決することもできます

var DumpCar = function(){this.init.apply(this,arguments)}
var f=function(){};
f.prototype = Car.prototype;
DumpCar.prototype = new f();    /* fはinitを実行してないので継承だけが行われる*/
解決方法3

jQueryにように、new 指定なしでインスタンス化をするなら instanceof を活用することでも解決できます。
(new 指定した場合は initは起動されません)

/* 親 */
var Car = function(){
   if(!(this instanceof Car))this.init.apply(this,arguments)
}
・・・

/* 子 */
var DumpCar = function(){
   if(!(this instanceof Car))this.init.apply(this,arguments)
}
DumpCar.prototype = new Car();  /* 継承 initは起動しない */
・・・

/* 実行 */
DumpCar({tire:6}) //もしくは、new DumpCar().init({tire:6})

親コンストラクタ(init)の実行

親クラス名をリテラルで指定しapplyに頼ってます。

DumpCar.prototype.init = function(p){
    Car.prototype.init.apply(this,arguments)
    return this;
}

以下のように記述したいところです。次エントリで解決します。

DumpCar.prototype.init = function(p){
    this.$super.init(p);
    return this;
}

次エントリにつづく...