matchをtestに置き換える場合はglobalフラグに注意

DOM Rangeを使って複数のノードを上の階層に上書きするで紹介したキーワードをハイライトにするコードに不具合があったのでメモ。firebug必須のエントリー。



http://upload0.dyndns.org/up/2/_/ のページで下のスクリプトを実行すると、"zip"にマッチした部分が黄色くなるはず。

//キーワードにヒットしたらハイライト
var k = document.evaluate('//td[@class="name"]', document, null, 7, null);
for(i=0;i<k.snapshotLength;i++){
  if(/ZIP/gi.test(k.snapshotItem(i).textContent)){
    k.snapshotItem(i).style.backgroundColor = 'yellow';
  }
}

しかし出力をよく見ると全ての"zip"が黄色になっていない。なんでだろ。ちょこちょこいじってgフラグをはずしてみたら正常に処理された。ここでid:javascripterのコメントが頭に浮かんだ。

https://developer.mozilla.org/jaを見ると分かりますが、 RegExp#execでのgオプションは別の意味を持っています。

ふむふむ。execとtestはRegExp一家のメソッド兄弟なのでgフラグが問題だなと当たりをつけて、調査開始。

正規表現で "g" フラグを使用する場合、同じ文字列で成功するマッチを見つけるために exec メソッドを複数回使うことができます。そのときの検索は、正規表現の lastIndex プロパティで指定された、str の部分文字列から開始されます。
exec - MDC

ほうほう。gフラグとlastIndexは深い関係があるのか。exec - MDCのページを参考にしながらlastIndexが表示されるようにさっきのコードを書き換えてみた。
http://upload0.dyndns.org/up/2/_/ のページで実行してみてください。

var k = document.evaluate('//td[@class="name"]', document, null, 7, null);
for(i=0;i<k.snapshotLength;i++){
  var re = /ZIP/gi;
  var t = re.test(k.snapshotItem(i).textContent);
  if(t){
    console.log(i + ' : ' + 'lastIndex' +' > ' + re.lastIndex)
    k.snapshotItem(i).style.backgroundColor = 'yellow';
  }
}

firebugのコンソールに表示された実行結果とアップローダの黄色くなったところを観察する。
lastIndexとは『次のマッチが始まる位置。』すなわち今回の場合、"zip"のpの位置を表す数字。execやtestはgフラグがある場合にlastIndexを密かに保持して、次の検索のときにlastIndexの場所から検索を開始する。ということを頭にいれて出力をもう一度観察するとなぜ全ての"zip"が黄色くなっていないかがわかってきた。

ファイル名 lastIndex
20090103.zip 12
毎月新聞を風呂で読むひと.ZIP 16
にん.zip
20090103_hoge.zip 17

上の表の例でいうと"にん.zip"が黄色くなるにはtestされる前のlastIndexが3以下である必要があった。そして『lastIndex が文字列の長さよりも大きければ、regexp.test 及び regexp.exec は失敗し、lastIndex は 0 にセットされ』*1最初の条件に戻る。で、冒頭のコードに戻って、gフラグをはずしてやればlastIndexは保持されなくなるので、今回の問題は起きない。


なぜこの問題が起きたのかというと/雨|テメ|バナナ/.testのような複数の条件で検索したい場合にgフラグをつけなければいけないような錯覚を起こしたからである。(matchのgフラグの意味と混同していた。)今回のようにtrue/false判定をしたいだけなら、gフラグは必要ない。(matchの場合も今回は必要ない。)

まとめ

冒頭のコードを違う点は、gフラグをはずしただけ。

//キーワードにヒットしたらハイライト
var k = document.evaluate('//td[@class="name"]', document, null, 7, null);
for(i=0;i<k.snapshotLength;i++){
  if(/ZIP/i.test(k.snapshotItem(i).textContent)){
    k.snapshotItem(i).style.backgroundColor = 'yellow';
  }
}

感想

なんとなくかっこよくみえるからという理由だけでmatchをtestに置き換えていたが考えが甘かった。