v1.6.0 Hashクラス

前回最後にHashクラスに言及したので見てみます。

【抜粋】
function $H(object) {
  return new Hash(object);
};

var Hash = Class.create(Enumerable, (function() {
  if (function() {
    var i = 0, Test = function(value) { this.key = value };
    Test.prototype.key = 'foo';
    for (var property in new Test('bar')) i++;
    return i > 1;
  }()) {
    function each(iterator) {
      var cache = [];
      for (var key in this._object) {
        var value = this._object[key];
        if (cache.include(key)) continue;
        cache.push(key);
        var pair = [key, value];
        pair.key = key;
        pair.value = value;
        iterator(pair);
      }
    }
  } else {
    function each(iterator) {
      for (var key in this._object) {
        var value = this._object[key], pair = [key, value];
        pair.key = key;
        pair.value = value;
        iterator(pair);
      }
    }
  }

  function toQueryPair(key, value) {
    if (Object.isUndefined(value)) return key;
    return key + '=' + encodeURIComponent(String.interpret(value));
  }

  return {
    initialize: function(object) {
      this._object = Object.isHash(object) ? object.toObject() : Object.clone(object);
    },

    _each: each,

    set: function(key, value) {
      return this._object[key] = value;
    },

    get: function(key) {
      return this._object[key];
    },

    unset: function(key) {
      var value = this._object[key];
      delete this._object[key];
      return value;
    },

    toObject: function() {
      return Object.clone(this._object);
    },

    keys: function() {
      return this.pluck('key');
    },

    values: function() {
      return this.pluck('value');
    },

    index: function(value) {
      var match = this.detect(function(pair) {
        return pair.value === value;
      });
      return match && match.key;
    },

    merge: function(object) {
      return this.clone().update(object);
    },

    update: function(object) {
      return new Hash(object).inject(this, function(result, pair) {
        result.set(pair.key, pair.value);
        return result;
      });
    },

    toQueryString: function() {
      return this.map(function(pair) {
        var key = encodeURIComponent(pair.key), values = pair.value;

        if (values && typeof values == 'object') {
          if (Object.isArray(values))
            return values.map(toQueryPair.curry(key)).join('&');
        }
        return toQueryPair(key, values);
      }).join('&');
    },

    inspect: function() {
      return '#<Hash:{' + this.map(function(pair) {
        return pair.map(Object.inspect).join(': ');
      }).join(', ') + '}>';
    },

    toJSON: function() {
      return Object.toJSON(this.toObject());
    },

    clone: function() {
      return new Hash(this);
    }
  }
})());

Hash.prototype.toTemplateReplacements = Hash.prototype.toObject;
Hash.from = $H;

最初の部分でいきなり躓きました。どうやらあらかじめprototypeにプロパティが設定されていて、インスタンス生成時または後に同名のプロパティを設定すると、別のプロパティになる場合があるということのようですが。手持ちの環境(Xp+IE,Firefox,Opera)では確認できませんでした。で、

http://dev.rubyonrails.org/browser/spinoffs/prototype/trunk/src

で調べようとしたら、最新(hash.js Revision 8139)からは消えてました^^; 「冗長」とか言われてます。

【hash.js Revision 8139】
function $H(object) { 
  return new Hash(object); 
}; 
 
var Hash = Class.create(Enumerable, (function() { 
 
  function toQueryPair(key, value) { 
    if (Object.isUndefined(value)) return key; 
    return key + '=' + encodeURIComponent(String.interpret(value)); 
   } 
  
  return { 
    initialize: function(object) { 
      this._object = Object.isHash(object) ? object.toObject() : Object.clone(object); 
    }, 
  
    _each: function(iterator) { 
      for (var key in this._object) { 
        var value = this._object[key], pair = [key, value]; 
        pair.key = key; 
        pair.value = value; 
        iterator(pair); 
      } 
    }, 
     :(中略)
  }
})()); 
  
Hash.prototype.toTemplateReplacements = Hash.prototype.toObject; 
Hash.from = $H; 

普通に_eachが設定されています。・・・v1.6.0の分岐は気にしないことにします(逃)。

initializeメソッドで大幅な方針転換が示されています。以前はオブジェクト(連想配列)そのものにHashクラスを継承させていました。このため、Hashクラスが持つメソッドと同名のプロパティは使用できない、値として関数は取れないという制限がありました。これを、オブジェクトを複製してプロパティとして持つことで解消しています。

【参考URL】http://d.hatena.ne.jp/susie-t/20060802/1154487988

このため、メンバの参照・追加・変更・削除をオブジェクトに対して直に行うことができなくなります。

【参考】これは不可。
var hash = $H({
  a: "A",
  b: "B"
});
alert(hash.a);//undefined
hash.c = "C";
hash.a = "AA";
delete hash.b;
hash.each(function(pair){
  alert(pair.key + ":" + pair.value); // a:A > b:B
})

メンバの操作は原則、Hashのメソッド経由で行います。参照>get、追加・更新>set、削除>unsetです。

【例】これはOK
var hash = $H({
  a: "A",
  b: "B"
});
alert(hash.get("a")); //A
hash.set("c", "C");
hash.set("a", "AA");
hash.unset("b");
hash.each(function(pair){
  alert(pair.key + ":" + pair.value); // a:AA > c:C
})

また、mergeメソッドはhashインスタンスが持つオブジェクトのコピーに引数のオブジェクトを上書きしたものを返却するだけとなり、更新はしなくなりました。更新する場合はupdateメソッドを使用します。

【例】
var hash = $H({
  a: "A",
  b: "B"
});
obj = {
  b: "BB",
  c: "C"
}
var mrg = hash.merge(obj);
alert(mrg.get("b")); // BB
alert(hash.get("b")); // B
var upd = hash.update(obj);
alert(upd.get("b")); // BB
alert(hash.get("b")); // BB

制限の解消のためとはいえ、大変なインターフェースの変更。これは泣いた人も多いんじゃなかろうか・・・。