Hashクラス

【抜粋】一部省略
var Hash = {
  _each: function(iterator) {
    for (key in this) {
      var value = this[key];
      if (typeof value == 'function') continue;

      var pair = [key, value];
      pair.key = key;
      pair.value = value;
      iterator(pair);
    }
  },
(省略)
  inspect: function() {
    return '#<Hash:{' + this.map(function(pair) {
      return pair.map(Object.inspect).join(': ');
    }).join(', ') + '}>';
  }
}

いわゆる連想配列を扱うクラスです。Hashクラスも、Enumerableクラスと同様、そのインスタンスが作成されることはありません。抽象クラス扱いです。ただ、実装方法は少し特殊です。他クラスに継承させるのではなく、$H関数によって返却されるオブジェクトがHashクラスを継承している、という形になります。Hashオブジェクトを作って各プロパティを追加するよりも、カスタムオブジェクトを作ってそれにHashを継承させたほうが手っ取り早い、ということなのでしょう。(2006/08/03追記。new Hash({name:value,・・・}); という手もあった気はしますが。メソッドを上書きされるのを恐れたのでしょうか。。。→2007/10/01追記 v1.5.〜ではこの方法が採られています。)

【例】
var hash = $H({zero:"零", one:"壱",two:"弐", three:"参"});
//hashはHashクラスを継承したオブジェクト。

$H関数の制限

※ここで言及している制限は、最新(v1.6.0以降)では改善されています。「v1.6.0 Hashクラス - Backstage of theater.js」を参照してください。

先に書いてしまいますが、$H関数に渡すことのできるカスタムオブジェクトには、以下の制限があります。

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

Enumerableクラス、Hashクラスが持つメソッドと同じ名前のプロパティは作れません。具体的には以下。

【参考】$H関数に渡すカスタムオブジェクトで使えないプロパティ名
each,all,any,collect,detect,findAll,grep,include,inject,invoke,
max,min,partition,pluck,reject,sortBy,toArray,zip,inspect,map,
find,select,member,entries,_each,keys,values,merge,toQueryString

使うと、返却されるオブジェクトからそのプロパティが消えます。(実際にはメソッドで上書きされている)

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

inspectというプロパティが消えました。inspectメソッドは機能しています。

  • 値として関数はとれない。

関数はメソッドとみなされ、各メソッドの処理対象となりません。

【参考】
var hash = $H({zero:"零", one:"壱", two:"弐",
               func:function(){alert("!");},
               three:"参"});
alert(Object.inspect(hash));
//#<Hash:{'zero': '零', 'one': '壱', 'two': '弐', 'three': '参'}> と表示される。
hash.func();
//"!"と表示される

funcメソッドは作成されますが、inspectの処理対象からは除外されています。

んー、後者はいいとして、前者は危険ですね・・・。うっかり使ってしまっても、気がつかないかもしれませんし。。。

2006/11/06追記。「prototype.jsのHashクラスの制約を回避 - Backstage of theater.js」に改修案を記述しました。

_eachメソッド

【抜粋】
  _each: function(iterator) {
    for (var key in this) {
      var value = this[key];
      if (typeof value == 'function') continue;

      var pair = [key, value];
      pair.key = key;
      pair.value = value;
      iterator(pair);
    }
  },

Enumerableクラスを継承する場合に必要なメソッド。自オブジェクトの各プロパティのプロパティ名と値を格納したオブジェクトを、引数の関数iteratorに渡して実行します。これにより、Enumerableから継承したメソッドで各プロパティを処理できます。

for inループにより自オブジェクトthisのすべてのプロパティ・メソッドを処理しています。値valueが関数(メソッド)であれば処理しません。pairに、プロパティ名keyと値valueを順に格納した配列を作成しています。更に、pairのkeyプロパティにプロパティ名key、valueプロパティに値valueを作成・格納しています。つまり、iterator内では、プロパティ名ならpair[0]とpair.key、値ならpair[1]とpair.valueのどちらでもアクセスできることになります。

【参考】
var hash = $H({zero:"零", one:"壱", two:"弐", three:"参"});
hash.each(function(pair){
  alert(pair[0] + "," + pair[1] + "," + pair.key + "," + pair.value);
  //"zero,零,zero,零"・・・と表示される
});

keysメソッド

【抜粋】
  keys: function() {
    return this.pluck('key');
  },

前述Enumerableクラスから継承したpluckメソッドにより、自オブジェクトのプロパティ名のみの配列を返却します。pluckが処理するのは前述_eachメソッド内で作成されるpairオブジェクトです。自オブジェクトthisそのものではないので注意してください。

【例】
var hash = $H({zero:"零", one:"壱", two:"弐", three:"参"});
var ret = hash.keys();
alert(ret);
//retは配列。"zero,one,two,three"と表示される。

valuesメソッド

【抜粋】
  values: function() {
    return this.pluck('value');
  },

こちらは自オブジェクトの値のみの配列を返却します。

【例】
var hash = $H({zero:"零", one:"壱", two:"弐", three:"参"});
var ret = hash.values();
alert(ret);
//retは配列。"零,壱,弐,参"と表示される。

mergeメソッド

【抜粋】
  merge: function(hash) {
    return $H(hash).inject($H(this), function(mergedHash, pair) {
      mergedHash[pair.key] = pair.value;
      return mergedHash;
    });
  },

自オブジェクトthisに引数hashをマージするメソッドです。引数hashの各プロパティを、injectメソッドにより、自オブジェクトthisの同一プロパティ名へ設定しています。よって、重複するプロパティ名があった場合、追加するhashのプロパティに上書きされます。

引数hashを$H関数で処理しているので、hashは単なるカスタムオブジェクトでもいいようです。ただ、なんでthisまで$H関数で処理しているのかは分からないのですが。。。→2006/08/08修正。thisの複製を作成し、thisを破壊しないようにするためですね。(同じ間違いを・・・orz)

【例】
var org = $H({zero:"零", one:"壱", two:"弐"});
var add = {two:"二", three:"参"}; //$Hで処理しなくても可
var ret = org.merge(add);
alert(Object.inspect(ret));
//retはHashオブジェクト(を継承)。
//#<Hash:{'zero': '零', 'one': '壱', 'two': '二', 'three': '参'}> と表示される。

toQueryStringメソッド

【抜粋】
  toQueryString: function() {
    return this.map(function(pair) {
      return pair.map(encodeURIComponent).join('=');
    }).join('&');
  },

自オブジェクトthisの各プロパティを、既存encodeURIComponent関数によりURIエンコードし、プロパティ名と値を「=」で連結した文字列にします。その上で各文字列を「&」で連結し、その文字列を返却します。

【例】
var hash = $H({zero:"零", one:"壱", two:"弐", three:"参"});
alert(hash.toQueryString());
//"zero=%E9%9B%B6&one=%E5%A3%B1&two=%E5%BC%90&three=%E5%8F%82"と表示される。

ところで、pairはArrayクラスのオブジェクトであるため、そのmapメソッドが処理するのはpair[0]とpair[1]となり、pair.keyとpair.valueは処理されません。

inspectメソッド

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

Hashを文字列化するメソッド。Object.inspectメソッド経由で使用されることが前提です。・・・このくらいの処理ならもう説明は不要でしょうか^^; 例についても、前述の他のメソッドにあるので割愛します。

HashクラスのEnumerableクラスから継承したメソッド使用サンプル(2006/09/04追記)

HashクラスのEnumerableクラスから継承したメソッドについて、使用サンプルを羅列します。いっぺんには書けないので少しずつ追加していきます。・・・そのうち字数制限に引っかかるかも^^; そのときは別の日に移します。。。

基本的に「Enumerableクラス(2) - Backstage of theater.js」以降の例で、クロージャの引数valueが、pairに変わるだけです。pairオブジェクトは前述したように、keyプロパティとvalueプロパティを持ちます。

allメソッドサンプル

var hash = $H({zero:"零", one:"壱", two:null, three:"参"});
var ret = hash.all(function(pair, index){
  alert(index + ":" + pair.key + ":" + pair.value);
  return pair.value;
});
//"0:zero:零","1:one:壱", "2:two:null"まで表示される。 
alert(ret);//"false"が表示される。
//two:nullがtwo:"弐"なら最後まで表示されて、"true"が表示される。

anyメソッドサンプル

var hash = $H({zero:null, one:false, two:"弐", three:undefined});
var ret = hash.any(function(pair, index){
  alert(index + ":" + pair.key +":" + pair.value);
  return pair.value;
});
//"0:zero:null","1:one:false", "2:two:弐"まで表示される。 
alert(ret);//"true"が表示される。
//two:"弐"がtwo:nullなら最後まで表示されて、"false"が表示される。

collectメソッドサンプル

var hash = $H({zero:"零", one:"壱", two:"弐", three:"参"});
var ret = hash.collect(function(pair, index){
  return index + ":" + pair.key + ":" + pair.value;
});
alert(ret);
//retは配列。"0:zero:零,1:one:壱,2:two:弐,3:three:参"が表示される。

配列とカスタムオブジェクト(連想配列)

配列はArrayクラスのインスタンスです。ただし、以下の前提があります。

配列のインデックスは0から連続した自然数である

勘違いしがちなのは、lengthプロパティがオブジェクトのメンバ数を表す、と思ってしまうことです。実際には最大の自然数のインデックス+1の値が格納されます。上の前提により、これが配列の要素数を表すことになります。

var ary = new Array();
ary[-2] = "N"; //インデックスが自然数でないので配列の要素ではない。
ary[3] = "Y"; //配列の要素
ary[3.5] = "N"; //インデックスが自然数でないので配列の要素ではない。
alert(ary.length); //"4"と表示される。
                   //ary[3]があるとary[0],ary[1],ary[2]はあるとみなされる。
                   //ary[-2],ary[3.5]は要素としてカウントされない。

つまり、Arrayクラスのオブジェクトは、追加されるメンバのプロパティ名を監視し、それが自然数で、lengthプロパティ以上であった場合、lengthプロパティをその値+1に変更する、という機能を持っています。

var ary = new Array();
ary[2] = "Y"; 
alert(ary.length); //"3"と表示される。
ary[4] = "Y";
alert(ary.length); //"5"と表示される。
ary[3] = "Y";
alert(ary.length); //"5"と表示される。

配列の各要素を扱うコードは以下のようになります。*1

var ary = new Array();
ary[2] = "二"; 
ary[4] = "四";
for(var i = 0; i < ary.length; i++){
  if(!(i in ary)){ 
    continue; //メンバとして存在しなければ処理せず次の要素へ
  }
  alert(ary[i]); //'二', '四'が順に表示される。
}

ここでiの初期値をマイナスにしたり、i++をi+=0.5にしたりするのは適切でない、ということになります。また、ここでfor inループを使うのも適切ではありません。

配列の省略記法として以下があります。

var ary = ["零", "壱", "弐", "参"];

これは以下と同じです。

var ary = new Array();
ary[0] = "零"; 
ary[1] = "壱";
ary[2] = "弐"; 
ary[3] = "参";

応用すると、多次元配列も簡単に書けるようになります。

var ary = [0, [10, 11], [[200, 201], [210, 211]]];
alert(ary[2][0][1]); //'201'が表示される

次に、カスタムオブジェクト(連想配列)です。こちらは配列のようにlengthが自動的に更新されるような機能を持ちませんので、オブジェクトのすべてのメンバを処理するためにはfor inループを使います。

var obj = new Object();
obj.zero = "零";
obj.one = "壱";
obj.two = "弐";
obj.three = "参";
for(var i in obj){
  alert(i + ":" + obj[i]);
  //"zero:零","one:壱","two:弐","three:参"が順に表示される
}

obj.zeroをobj["zero"]と書いても同じです。違いとして、前者には変数として許容できる名前のみが使用できるのに対し、後者はほぼ何でも設定が可能、ということです。また、変数の値の文字列を使用したい場合にも有効です。

obj["zero"] = "零";
obj[1] = "壱";
//obj.1 = "壱"; →これはだめ。1は変数名にならない。
obj["弐"] = "弐";
var str = "three"
obj[str] = "参";

カスタムオブジェクトの省略記法として以下があります。

var obj = {zero:"零", one:"壱", two:"弐", three:"参"};

プロパティ名を"や'でくくれば日本語や空白も設定可能です。配列と同様、入れ子にすることができます。

var obj = {zero:"零", one:{two:"壱弐", three:"壱参"}};
alert(obj.one.two); //"壱弐"と表示される。

*1:要素の存在判定を==nullで行っていたのをinに置き換えました。こちらのほうが妥当だと思います。

$H関数

【抜粋】
function $H(object) {
  var hash = Object.extend({}, object || {});
  Object.extend(hash, Enumerable);
  Object.extend(hash, Hash);
  return hash;
}

空のオブジェクトに、引数object(nullの場合は空オブジェクト)を継承させています。これにより引数objectの複製を得ます。これにEnumerableクラスとHashクラスを継承させ、返却します。

複数のクラスを「継承する」というのも変ですが。そういう意味では言葉そのままの「拡張する」というほうが正確なのかもしれません。(もう修正するのはツライのでこのままいきますが^^;)

前述Hashクラスでかなり説明してしまったので、そちらを参照してください。