v1.6.0 Classオブジェクト

【抜粋】
/* Based on Alex Arnell's inheritance implementation. */
var Class = {
  create: function() {
    var parent = null, properties = $A(arguments);
    if (Object.isFunction(properties[0]))
      parent = properties.shift();

    function klass() {
      this.initialize.apply(this, arguments);
    }

    Object.extend(klass, Class.Methods);
    klass.superclass = parent;
    klass.subclasses = [];

    if (parent) {
      var subclass = function() { };
      subclass.prototype = parent.prototype;
      klass.prototype = new subclass;
      parent.subclasses.push(klass);
    }

    for (var i = 0; i < properties.length; i++)
      klass.addMethods(properties[i]);

    if (!klass.prototype.initialize)
      klass.prototype.initialize = Prototype.emptyFunction;

    klass.prototype.constructor = klass;

    return klass;
  }
};

Class.Methods = {
  addMethods: function(source) {
    var ancestor   = this.superclass && this.superclass.prototype;
    var properties = Object.keys(source);

    if (!Object.keys({ toString: true }).length)
      properties.push("toString", "valueOf");

    for (var i = 0, length = properties.length; i < length; i++) {
      var property = properties[i], value = source[property];
      if (ancestor && Object.isFunction(value) &&
          value.argumentNames().first() == "$super") {
        var method = value, value = Object.extend((function(m) {
          return function() { return ancestor[m].apply(this, arguments) };
        })(property).wrap(method), {
          valueOf:  function() { return method },
          toString: function() { return method.toString() }
        });
      }
      this.prototype[property] = value;
    }

    return this;
  }
};
【参考サイト】http://d.hatena.ne.jp/kazu-yamamoto/20071024/1193195233

詳しい解説は上記サイトにありますので、気が付いたことだけメモ。

随分変わってしまいましたね・・・。昔はたった7行だったのに^^;

【参考サイト】http://d.hatena.ne.jp/susie-t/20060710/1152510376

こんなことになったのはすべて、JAVAでいえばsuperにあたる機能を実装しようとしたからです。(逆に言うと、superなんて使わなければもとの7行コードでも十分な気がします)

とりあえず使ってみます。

【例】
var Animal = Class.create({
  weight: 10,
  initialize: function(w){
    this.weight = (w || 50);
  },
  eat: function(){
    alert("Animal : eat");
  },
  move: function(){
    alert("Animal : move");
  }
});

var Dog = Class.create(Animal, {
  initialize: function($super, w){
    $super(w);
  },
  eat: function($super){
    $super();
    alert("Dog : eat");
  },
  move: function(c){
    alert("Dog : move : " + c);
  }
});

var a = new Animal();
alert(a.weight);
a.eat();
var d = new Dog(100);
alert(d.weight);
d.eat();
d.move(5);
【結果(alert表示文字列の順)】
50
Animal : eat
100
Animal : eat
Dog : eat
Dog : move : 5

Class.create自体にカスタムオブジェクトを渡すことで、すぐにクラス定義が記述できるようになってます。以前はClass.createでオブジェクトを取得し、Object.extend等でprototypeプロパティを設定していました。

継承する場合は、第一引数を親クラス(関数)、第二引数をクラス定義用カスタムオブジェクトにします。

上記Dogクラスのinitialize, eatメソッドの第一仮引数に$superを設定しています。この名前は固定です。ここに設定される関数を実行すると、親クラスにある同名のメソッドが実行されます。ただし、メソッド呼び出し時の引数には、この$superにあたるものは必要ありません。これはprototype.jsが涙ぐましい努力によって、第一仮引数が$superである場合は、そこに親クラスの同名メソッド*1を入れ、第二仮引数以降に実行時引数を入れる、ということをしてくれているからです。

なので、子クラスだからといってすべてのメソッドの第一仮引数が$superである必要はありません。親クラスメソッドを使わないのなら、普通に仮引数を設定すればOKです。

JAVAだとsuper.methodとして別名のメソッドも扱えるのですが、この$superではそれはできません。・・・無理やりやると以下になります。

【参考】無理やり親クラスの別メソッドを実行する。
        上記Dog.moveを以下のように変更。
  move: function(c){
    alert("Dog : move : " + c);
    this.constructor.superclass.prototype.eat.bind(this)();
    //"Animal : eat"と表示される。
  }

こっちのほうが汎用性があるかも^^;

以下はコードを見て気が付いたこと。(基本的なことも含めて。)

【抜粋】
    var ancestor   = this.superclass && this.superclass.prototype;

この場合の動作の理解があいまいでした。これは以下とほぼ同義なんですね。

    var ancestor   = (this.superclass) ? this.superclass.prototype : null;

または

    var ancestor = null;
    if(this.superclass != null){
      ancestor = this.superclass.prototype;
    }

以下はちょっと悩みました。

【抜粋】
    if (!Object.keys({ toString: true }).length)

Object.keysはオブジェクトのプロパティ名の配列を返す拡張メソッド*2。それはいいとして、コードだけ見ると必ずfalseになる気が。

じつはこれは、toStringという名前のプロパティが、列挙可能になるかどうかを判定しているもの。

【参考サイト】http://developer.mozilla.org/ja/docs/Core_JavaScript_1.5_Reference:Global_Objects:Object:propertyIsEnumerable

FireFoxOperaはfalseですが、IEはtrueになります。つまり、IEはtoStringという名前のプロパティを自動的に列挙不能にしているのです(ちなみにvalueOfも)。

【参考】上記は以下と同じ
    if(!{ toString: true }.propertyIsEnumerable("toString"))

最後に頭が痛かったのは、Function.wrapメソッド。

【抜粋】
        var method = value, value = Object.extend((function(m) {
          return function() { return ancestor[m].apply(this, arguments) };
        })(property).wrap(method), {
          valueOf:  function() { return method },
          toString: function() { return method.toString() }
        });

wrapメソッドの定義は以下。

【抜粋】Function.wrapメソッド
Object.extend(Function.prototype, {
   :(省略)
  wrap: function(wrapper) {
    var __method = this;
    return function() {
      return wrapper.apply(this, [__method.bind(this)].concat($A(arguments)));
    }
  },
   :(省略)
});

wrapメソッドは、引数の関数wrapperを実行するクロージャを返却します。wrapper実行時引数は第一引数が自関数、第二引数以降がクロージャ実行時引数です。

ちょっと混乱したのは、「__method.bind(this)」の部分。でも良く考えてみると、これでいいのですね。もし以下のようにすると

  wrap: function(wrapper) {
    var __method = this;
    return function() {
      return wrapper.apply(this, [__method].concat($A(arguments)));
    }
  },

前述$super実行時、自クラスのプロパティ・メソッドが使用できないことになります。__methodはクロージャ参照なので、そのまま実行するとwindowオブジェクトのメンバとして実行されます。これを解消するためbind(this)が必要なわけです。

*1:実際はそれを実行する関数

*2:昔は同様のケースでは$H(obj).keys()としていた。ただ、これだとメソッド(メンバが関数の場合)は無視される。