prototype.jsのHashクラスの制約を回避

2008/01/16 追記。v1.6.0で解消。

ここで言及した制限はv1.6.0で解消されています。解決策はやはりオブジェクトをプロパティとして持つというものでした。

【参考】v1.6.0 Hashクラス - Backstage of theater.js

以下保存。

※v1.5.0に対応しました。
Hashクラス - Backstage of theater.js」で書いたように、prototype.jsの$H関数に渡すことのできるオブジェクトには、以下の制限があります。

  • すでに実装されているメソッドと同じ名前のプロパティは作れない。
  • 値として関数はとれない。

特に前者の条件は、汎用関数を作成する場合や、ユーザ入力を扱う場合にネックになります。プログラマがプロパティ名を完全にコントロールできていればいいのですが、そうでない場合は使いにくいです。

回避策としてヒントになるのは、同じくprototype.jsElement.ClassNamesクラスです。ここでは、オブジェクト生成時に引数(要素)をプロパティに登録し、eachメソッド等ではそのプロパティ*1を扱うという形になっています。

これを参考に改修案を提示します。もとのHashクラス、$H関数を改修するのは影響が大きいと思われるので、新たにHandleHashクラスと$HH関数を追加することにします。

【改修案】HandleHashクラス、$HH関数追加(v1.5.0対応)
var HandleHash = Class.create();
Object.extend(HandleHash.prototype, Hash.prototype);
Object.extend(HandleHash.prototype, {
  initialize: function(object){
    this.hash = (object || {});
  },
  
  _each: function(iterator) {
    for (key in this.hash) {
      var value = this.hash[key];
      var pair = [key, value];
      pair.key = key;
      pair.value = value;
      iterator(pair);
    }
  },
  
  merge: function(hash) {
    return $HH(hash).inject(this.hash, function(mergedHash, pair) {
      mergedHash[pair.key] = pair.value;
      return mergedHash;
    });
  },
  
  remove: function() {
    var result;
    for(var i = 0, length = arguments.length; i < length; i++) {
      var value = this.hash[arguments[i]];
      if (value !== undefined){
        if (result === undefined) result = value;
        else {
          if (result.constructor != Array) result = [result];
          result.push(value)
        }
      }
      delete this.hash[arguments[i]];
    }
    return result;
  }
});
function $HH(object){
  if(object && object.constructor == HandleHash) return object;
  return new HandleHash(object);
}

HandleHashクラスはEnumerableおよびHashクラスを継承しています。オブジェクト生成時の引数には連想配列(カスタムオブジェクト)を取ります。これをhashプロパティに格納します。_eachメソッドをオーバーライドし、hashプロパティの各メンバを扱うようにします。これでほぼすべてのメソッドが使えるようになるのですが、mergeメソッドとremoveメソッドはthisを直接使用しているため、hashプロパティを使用するように変更しています。

また、_eachメソッドで関数をメンバとして扱わないという命令を外しています。これにより、メンバとして関数も扱えます。(以下の例では分かりませんが^^;)

$HH関数は引数のオブジェクト使用してHandleHashオブジェクトを生成し、これを返却します。

以下、使用例です。

【例】
<html>
<head>
<title></title>
<script language="javascript" src="prototype.js" charset="utf-8"></script>
<script>
//クラス・関数追加
var HandleHash = Class.create();
Object.extend(HandleHash.prototype, Enumerable);
Object.extend(HandleHash.prototype, Hash);
Object.extend(HandleHash.prototype, {
  initialize: function(object){
    this.hash = (object || {});
  },
  
  _each: function(iterator) {
    for (key in this.hash) {
      var value = this.hash[key];
      var pair = [key, value];
      pair.key = key;
      pair.value = value;
      iterator(pair);
    }
  },
  
  merge: function(hash) {
    return $HH(hash).inject(this.hash, function(mergedHash, pair) {
      mergedHash[pair.key] = pair.value;
      return mergedHash;
    });
  },

  remove: function() {
    var result;
    for(var i = 0, length = arguments.length; i < length; i++) {
      var value = this.hash[arguments[i]];
      if (value !== undefined){
        if (result === undefined) result = value;
        else {
          if (result.constructor != Array) result = [result];
          result.push(value)
        }
      }
      delete this.hash[arguments[i]];
    }
    return result;
  }

});

function $HH(object){
  return new HandleHash(object);
}

//【使用例】
var handleHash = $HH({zero:"零", one:"壱", two:"弐", inspect:"文字列化", three:"参"});
alert(Object.inspect(handleHash));
//#<Hash:{'zero': '零', 'one': '壱', 'two': '弐', inspect:'文字列化', 'three': '参'}> 
//と表示される。

//【参考】通常のHash。プロパティ名「inspect」は使用不可(消える)。
var hash = $H({zero:"零", one:"壱", two:"弐", inspect:"文字列化", three:"参"});
alert(Object.inspect(hash));
//#<Hash:{'zero': '零', 'one': '壱', 'two': '弐', 'three': '参'}> と表示される。
</script>
</head>
<body>
</body>
</html>

プロパティ名として「inspect」が使用可能になっています。

メンバの追加・変更方法は以下の3通りがあります。

  • 引数として渡したオブジェクトに直接行う。
  • 生成したHandleHashオブジェクトのhashプロパティに対して行う。
  • mergeメソッドを使用する。
【例】上記例の使用例部分のみ記述
//【使用例】
var hash = {zero:"零"};
var handleHash = $HH(hash);
hash['one'] = "壱";
handleHash.hash['two'] = "弐";
handleHash.merge({inspect:"文字列化", three:"参"});
alert(Object.inspect(handleHash));
//#<Hash:{'zero': '零', 'one': '壱', 'two': '弐', inspect:'文字列化', 'three': '参'}> 
//と表示される。

この場合、HandleHashオブジェクトそのものにメンバ追加するのは不可です。

最近はもっぱらこの$HHを使用しています。いちいちプロパティ名について考えなくてすむので・・・。

*1:正確にはそのclassプロパティを配列化したもの