風柳メモ

ソフトウェア・プログラミング関連の覚書が中心

JavaScriptでプロパティの継承が出来るクラスっぽいものを実装する試み

同じような/より良い実装は星の数ほどあると思うけれども、個人的な覚書を兼ねて。

クラス(関数)作成用関数

var makeClass=(function(){
    var origin_constructor=null;
    var name_to_class={};
    var change_toString=function(target,name){
        var _toString=target.toString;
        target.toString=function(){
            return _toString.apply(this,arguments).replace(/\x28/,name+'\x28');
        };
    };
    var update_properties=function(obj_constructor,prop_assoc){
        var _prototype=obj_constructor.prototype;
        for (var key in prop_assoc) {
            if (!prop_assoc.hasOwnProperty(key)) continue;
            _prototype[key]=prop_assoc[key];
        }
    };
    var makeClass=function(base,prop_assoc,name){
        if (!prop_assoc) prop_assoc={};
        var obj_constructor=function(){};
        obj_constructor.__for_class__=true;
        var super_constructor=null;
        if (base) {
            switch (typeof base) {
                case    'string'    : if (name_to_class[base]) super_constructor=name_to_class[base].__constructor__; break;
                case    'function'  : super_constructor=(base.__constructor__)?base.__constructor__:base; break;
                case    'object'    : super_constructor=base.constructor; break;
            }
            if (super_constructor&&super_constructor.__for_class__) {
                try{
                    obj_constructor.prototype=new super_constructor();
                    obj_constructor.prototype.__super__=super_constructor;
                }
                catch (e){
                    super_constructor=null;
                }
            }
        }
        if (!super_constructor&&origin_constructor) {
            obj_constructor.prototype=new origin_constructor();
            obj_constructor.prototype.__super__=origin_constructor;
        }
        update_properties(obj_constructor,prop_assoc);
        obj_constructor.prototype.constructor=obj_constructor;
        
        var __class__=function(){
            var self=new obj_constructor();
            self.__class__=__class__;
            if (typeof self.__init__=='function') self.__init__.apply(self,arguments);
            return self;
        };
        obj_constructor.__class__=__class__;
        __class__.__constructor__=obj_constructor;
        
        __class__.update_properties=function(prop_assoc){
            update_properties(obj_constructor,prop_assoc);
        };
        
        if (typeof name=='string') {
            __class__.__name__=obj_constructor.__name__=name;
            change_toString(__class__,name);
            change_toString(obj_constructor,name);
            name_to_class[name]=__class__;
        }
        return __class__;
    };
    origin_constructor=makeClass(null,{},'Class').__constructor__;
    
    return makeClass;
})();   //  end of makeClass()
使い方

/* 【クラス(関数)作成】 */
var ClassFoo = makeClass( base, prop_assoc, name );
// base: 継承元クラス(無い場合はnull)
// prop_assoc: プロパティ定義用連想配列(__init__を定義しておくと、オブジェクト作成時にコールされる)
// name: 一意なクラス名


/* 【オブジェクト作成】 */
var ObjectFoo = ClassFoo( args );
// args: __init__プロパティ(メソッド)に渡す引数


/* 【プロパティ(メソッド)コール】 */
ObjectFoo.prop( prop_args );
// prop: クラス作成時に定義したプロパティ(メソッド)名
// prop_args: プロパティ(メソッド)に渡す引数


/* 【クラスのプロパティ追加・更新】 */
ClassFoo.update_properties( prop_assoc );
// prop_assoc: プロパティ定義用連想配列

使用例
/* 【クラス(関数)作成】 */
var ClassA=makeClass(null,{
    __init__ : function(a){ // __init__はオブジェクト作成時にコールされる
        var self=this;
        self.a=a;
    }

,   alert : function(name){ // プロパティ名 : 関数の形で定義
        var self=this;
        alert(self[name]);
    }
},'ClassA');

var ClassB=makeClass(ClassA,{ // ClassAを継承
    __init__ : function(a,b){ // ClassAの__init__を上書き
        var self=this;
        self.a=a;
        self.b=b;
    }

    // ※ alertプロパティは未定義→ClassAのものが参照される

,   sum : function(){ // 新規プロパティ
        var self=this;
        alert(self.a+'+'+self.b+'='+(self.a+self.b));
    }
},'ClassB');

/* 【オブジェクト作成】 */
var ObjectA=ClassA(1,2), ObjectB=ClassB(3,4);

/* 【プロパティ(メソッド)コール】 */
// ObjectA
ObjectA.alert('a'); // 1
ObjectA.alert('b'); // undefined
//ObjectA.sum(); // [Error] ObjectA.sum is not a function

// ObjectB
ObjectB.alert('a'); // 3
ObjectB.alert('b'); // 4
ObjectB.sum(); // 3+4=7

/* 【クラスのプロパティ追加・更新】 */
ClassA.update_properties({
    alert : function(name){ // alertを上書き
        var self=this;
        alert(name+' => '+self[name]);
    }

,   test : function(){ // 新規プロパティ(ClassBにも無し)
        alert('test()');
    }

,   sum :   function(){ // 新規プロパティ(ClassBに同名のプロパティ有り)
        alert('sum() is not supported');
    }
});

/* 【ClassA更新後のプロパティ(メソッド)コール】 */
// ObjectA
ObjectA.alert('a'); // a => 1
ObjectA.test(); // test()
ObjectA.sum(); // sum() is not supported

// ObjectB
ObjectB.alert('a'); // a => 3
ObjectB.test(); // test()
ObjectB.sum(); // 3+4=7
仕様上の注意
  • makeClass()の第1引数:継承元クラスとして指定可能なのは、null(継承無し)、および、makeClass()によって作成されたクラス(関数)(これは既存クラスから作成したオブジェクトや既存クラス作成時に指定した一意なクラス名でも代用可)に限る(例えばObjectやArrayその他一般のオブジェクト、リテラル等は不可)。
  • makeClass()の第3引数:一意のクラス名は、実は省略可(ただし省略すると、第1引数のクラス名(文字列)指定が不可になったり、クラス(関数)やオブジェクトのconstructorを(toString()して)見た時に解り難くなるなど、便宜上いくつか問題が出るかも知れないので注意)。
  • クラス(関数)作成時に__init__プロパティ(メソッド)を定義しておけば、クラス(関数)を呼出してオブジェクトを作成する時(作成処理の最後)にこれがコールされる。その際、クラス(関数)で指定した引数がそのまま渡される。
  • プロパティは、同名のものがある場合、オブジェクト自身のプロパティ>クラスのプロパティ定義>継承元クラスのプロパティ定義の優先順位で参照される。
  • クラスのプロパティをupdate_properties()等で追加・更新した場合、その影響は当該クラスから直接作成されたオブジェクトの他、当該クラスを継承した下位クラス及びそれから作成されたオブジェクトにも及ぶ(ただし、優先順位の関係で、下位クラスやオブジェクトが、自身のものとして同名のプロパティを持つ場合は影響を受けない)。
  • 下位クラス及びそれから作成されたオブジェクトに対するプロパティの追加・更新は、上位(継承元)クラス及びそれから作成されたオブジェクトには影響を及ぼさない。

ところで

new無しでも有りでもオブジェクトが作成できるコンストラクタは、

var Const=function(){
    if (!(this instanceof Const)) return new Const();
};
Const.prototype.test=function(){
    alert('OK');
};

new Const().test(); // OK
Const().test(); // OK

みたいな感じで作れるけれども、コンストラクタに任意の引数を指定したい場合はどうすれば良いのだろう?

var Const=function(){
    if (!(this instanceof Const)) return new Const(); // 【???ここをどう書くかが思い付かない???】
    this.__init__.apply(this,arguments);
};
Const.prototype.__init__=function(){
    alert(arguments.length);
};

new Const(1,2,3); // 3
Const(1,2,3); // 0 [NG]

とりあえず、上の方では、

var Const=function(){};
Const.prototype.__init__=function(){
    alert(arguments.length);
};

var Class=function(){
    var self=new Const();
    self.__init__.apply(self,arguments);
    return self;
};

new Class(1,2,3); // 3
Class(1,2,3); // 3

のようにして関数を一つかます形で実装しているけれども。