thisもnewもprototypeも使わない擬似クラス設計

たとえばこんな感じか。

function defined(v) { return (v !== undefined && v !== null) }

// ** objからキー _init に格納されるオブジェクトを引き抜き、
// ** objの残りメンバで破壊的に拡張して返す。
// ** objにキー _init が無い場合は空オブジェクトを新規作成して拡張
function build(obj) {
    var base;
    var key;
    if(defined(obj._init)) {
        base = obj._init;
        delete obj._init;
    }else {
        base = {};
    }
    for(key in obj) {
        if(!obj.hasOwnProperty(key)) continue;
        base[key] = obj[key];
    }
    return base;
}

// ** 基本クラス
var Figure = (function() {
    var private_class_variable = 0;
    var private_class_method = function() {};

    var thisclass = build({
        public_class_variable: 0,
        default_x: 0,
        default_y: 0,

        // ** コンストラクタ
        _init: function(name, init_x, init_y, constructor_arg) {
            var private_instance_variable = 0;
            var private_instance_method = function() {};
            var x = defined(init_x) ? init_x : thisclass.default_x;
            var y = defined(init_y) ? init_y : thisclass.default_y;

            var self = {
                public_instance_variable: 0,
                public_instance_method: function() {},
                toString: function() {
                    return self.getName() + ": x = " + self.getX() + ', y = ' + self.getY();
                },
                print: function() {
                    console.log(self.toString());
                },
                getName: function() { return name },
                getX: function() { return x },
                getY: function() { return y }
            }
            return self;
        },
        
        public_class_method: function() {},
    });
    return thisclass;
})();

// ** 使用例 1
(function() {
    var a = Figure("a", 10, 4);
    var b = Figure("b", 5);
    Figure.default_x = 109;
    Figure.default_y = -10;
    var c = Figure("c");
    a.print(); // => a: x = 10, y = 4   
    b.print(); // => b: x = 5, y = 0    
    c.print(); // => c: x = 109, y = -10
})();

// ** 派生クラス
var Circle = (function() {
    var SUPER = Figure; // ** 分かりやすさのため、先頭付近で親クラスを宣言
    var thisclass = build({
        default_radius: 10,

        // ** コンストラクタ
        _init: function(name, init_x, init_y, radius) {
            var self = SUPER(name, init_x, init_y); 
            var super_toString = self.toString; // ** 親クラスのメソッドを退避
            if(!defined(radius)) radius = thisclass.default_radius;
            build({
                _init: self, // ** 親クラスのオブジェクトをベースに拡張
                getRadius: function() { return radius },
                toString: function() {
                    return super_toString() + ", R = " + self.getRadius();
                },
            });
            return self;
        }
    });
    return thisclass;
})();

// ** 使用例 2
(function() {
    var a = Circle("A", 9, 3, 3);
    var b = Circle("B", null,10);
    Figure.default_x = 999;
    Circle.default_radius = 50;
    var c = Circle("C");
    a.print(); // => A: x = 9, y = 3, R = 3     
    b.print(); // => B: x = 109, y = 10, R = 10 
    c.print(); // => C: x = 999, y = -10, R = 50
})();

ポイントは、

  • クラスはコンストラクタ関数とする
  • クラス変数はコンストラクタ関数の属性
  • レキシカル変数を使ってプライベート変数を実現
  • 継承は親クラスのインスタンスを作って適宜拡張する

thisを使ってハマることが多々あるのでこういうやり方のほうが性に合っているかもしれない。
って、こんなこと考えているヒマがあったらDartなりJSXなり勉強したほうがいいかも。

あと、インスタンス変数をレキシカル変数として用意するのはデバッグ時にいろいろ不便かもしれない。
そう考えるとインスタンス変数は素直にselfオブジェクトの属性としたほうがいいかも。
たしかにアクセス権限の管理はできないが、まあPerlでも似たようなもんだし。

ベンチマークやった → thisもnewもprototypeも使わない擬似クラス設計 (評価編) - DebugIto’s diary