JavaScript で C++ っぽいクラスを実装してみた

js で、C++ ライクなクラス

  • コンストラクタ(init)とデストラクタ(fin)が使える
    • 生成は 親親⇒親⇒子, 廃棄は子⇒親⇒親親 の順で実行
    • デストラクタ(fin)を明示的に呼ぶと、インスタンスの全てのプロパティが null で上書される(使えなくなる)。
    • window.onunload で、オートデストラク
  • 三階層まで継承できる(子<親<親親)
    • uu.Class("B : A", {}) や
    • uu.Class("C < B", {}) で簡単に継承できる
      • 継承が不要なら uu.Class("C", {}) で素のクラスを定義できる
  • 下位のクラスから上位のクラスのメソッドが呼べる
    • this.superMethod(arguments.callee, "呼び出すメソッド名", var_args...) または
    • this.superMethod(呼び出し元のメソッド名, "呼び出すメソッド名", var_args...)
  • 全部 public
  • 多重継承とか static メソッドとかは無し

を実装してみました

こんな感じ

DEMO

uu.Class("A", {
  init: function Ainit() {}, // コンストラクタ
  fin: function Afin() {}, // デストラクタ
  hoge: function Ahoge() {},
  huga: function Ahuga() {},
  piyo: function Apiyo() {}
});

uu.Class("B : A", { // B は A を継承  (C++ チックに)
  init: function Binit() {},
  fin: function Bfin() {},
  hoge: function Bhoge() {}
//  huga: function() {},
//  piyo: function() {}
});

uu.Class("C < B", { // C は B を継承  (Ruby チックに)
  init: function Cinit() {},
  fin: function Cfin() {},
  hoge: function Choge() {},
//  huga: function() {},
  piyo: function Cpiyo() {
    this.superMethod(arguments.callee, "piyo", 1, 2, 3); // Apiyo.call(this, 1, 2, 3) 相当
  }
});

// インスタンスの生成
var a = uu.factory("A", 1, 2); // new uu.Class.A(1, 2); でも同じ
var b = uu.factory("B", 1, 2); // Binit(Ainit()) 相当
var c = uu.factory("C", 1, 2); // Cinit(Binit(Ainit())) 相当

c.hoge(); // Choge() を呼ぶ
c.huga(); // Ahuga() を呼ぶ
c.piyo(); // Cpiyo(Apiyo()) を呼ぶ

// 明示的にインスタンスを破棄(全プロパティが null に設定される)
c.fin(); // Afin(Bfin(Cfin())) 相当

c.piyo(); // エラー「そんな関数ありません」

実装

// uu.Class - create a generic class
function uuclass(className, // @param String: "Class"
                            //             or "Class:SuperClass"
                            //             or "Class<SuperClass"
                 proto) {   // @param Hash(= void 0): prototype object
    var ary = className.split(/\s*[\x3a-\x40]\s*/), tmp, i,
        Class = ary[0], Super = ary[1] || "";

    uuclass[Class] = function uuClass() {
        var lv3 = this,
            lv2 = lv3.superClass || 0,
            lv1 = lv2 ? lv2.superClass : 0;

        uuclassguid(lv3);
        lv3.msgbox || (lv3.msgbox = uunop);
        uu.msg.register(lv3);

        // constructor(lv1 -> lv2 -> lv3)
        lv1 && lv1.init && lv1.init.apply(lv3, arguments);
        lv2 && lv2.init && lv2.init.apply(lv3, arguments);
               lv3.init && lv3.init.apply(lv3, arguments);

        // destructor(~lv3 -> ~lv2 -> ~lv1)
        lv3["~fin"] = lv3.fin || uunop;
        lv3.fin && uuevattach(win, "unload", function() {
            lv3.fin && lv3.fin();
        });
        lv3.fin = function wrapper() {
            lv3["~fin"]();
            lv2 && lv2.fin && lv2.fin.call(lv3);
            lv1 && lv1.fin && lv1.fin.call(lv3);

            // destroy them all
            for (var i in lv3) {
                lv3[i] = null;
            }
        };
    };
    uuclass[Class].prototype = proto || {};

    if (Super) {
        tmp = function() {};
        tmp.prototype = uu.Class[Super].prototype;
        uuclass[Class].prototype = new tmp;

        for (i in proto) {
            uuclass[Class].prototype[i] = proto[i];
        }
        uuclass[Class].prototype.constructor = uuclass[Class];
        uuclass[Class].prototype.superClass = uu.Class[Super].prototype;
        uuclass[Class].prototype.superMethod = superMethod;
    }

    function superMethod(from,             // @param Function: caller
                         to                // @param String:
                         /* var_args */) { // @param Mix: args
        var obj = this.superClass;

        // recurtion guard
        if (from === obj[to] || superMethod.caller === obj[to]) {
            obj = obj.superClass;
        }
        return obj[to].apply(this, uu.ary(arguments).slice(2));
    }
}

補足

  • 名前付きのメソッドを使っている場合は、 this.superMethod(arguments.callee, "メソッド名", var_args...) の arguments.callee の代わりにメソッド名を指定できます。
    • つまり、以下のようにも書けます。
  piyo: function Cpiyo() {
//  this.superMethod(arguments.callee, "piyo", 1, 2, 3);
    this.superMethod(Cpiyo, "piyo", 1, 2, 3);
  }
    • Function.caller が使える環境なら、arguments.callee は不要なのですが、Opera9.5 で Function.caller が使えず、代替方法も無いため、呼び出し元の情報を渡すような I/F になっています。
  • 継承元のクラス(SuperClass や SuperSuperClass)に動的に関数を追加しないで下さい。未定義の動作を引き起こします。
    • 動的に関数を追加したい場合は継承させずに使ってください。
  • シングルトンクラスを生成する uu.Class.singleton は継承をサポートしていません
    • uu.Class.singleton("B<A") と書いても、思い通りには動きませんのでご注意を。