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に置き換えていたが考えが甘かった。