JavaScript 正規表現の g フラグを使ったときの test() の挙動
さて、突然ですがクイズです。
JavaScript で const regexp = /[abc]/g
のあと regexp.test('aa')
を何度か実行すると戻り値はどうなるでしょうか。
const regexp = /[abc]/g
console.log(regexp.test('aa'))
// ???
console.log(regexp.test('aa'))
// ???
console.log(regexp.test('aa'))
// ???
test の想定外の挙動
正解は… true
が2回出力されたあと false
が1回出力される です。パッと正解できる人はどれだけいるでしょうか。私は無理でした😂
const regexp = /[abc]/g
console.log(regexp.test('aa'))
// true
console.log(regexp.test('aa'))
// true
console.log(regexp.test('aa'))
// false
// 以下繰り返し
動作を確認してみる
test
を exec
に変えて試してみると原因がわかります。ちなみに test
はマッチしたかどうかをブール値で返すのに対し、 exec はマッチした場合に結果オブジェクトを返します。
const regexp = /[abc]/g
regexp.exec('aa')
// ['a', index: 0, input: 'aa', groups: undefined]
regexp.exec('aa')
// ['a', index: 1, input: 'aa', groups: undefined]
regexp.exec('aa')
// null
よく見てみると index
が加算され、入力文字列の長さになると null
になっています。これが true
true
false
に相当するわけですね。
この動作は仕様通り
MDN を見てみるとこの動作は仕様通りであることがわかります。
グローバルフラグを持つ正規表現の test() の使用
正規表現にグローバルフラグが設定されている場合、 test() は正規表現が所有する lastIndex の値を加算します。
その後にさらに test(str) を呼び出すと、 str を lastIndex から検索します。 lastIndex プロパティは test() が true を返すたびに増え続けます。
メモ: test() が true を返す限り、 lastIndex は別な文字列をテストした場合であっても、リセットされません。
const regex = /foo/g; // "global" フラグを設定
// regex.lastIndex は 0 です。
regex.test('foo') // true
// regex.lastIndex は 3 です。
regex.test('foo') // false
// regex.lastIndex は 0 です。
regex.test('barfoo') // true
// regex.lastIndex は 6 です。
regex.test('foobar') //false
// regex.lastIndex は 0 です。
// (...以下略)
呼び出しごとに RegExp (regexp
) オブジェクトの中で lastIndex
が加算され、次はテスト文字列にかかわらずその位置から検索が始まるため、このような挙動になります。
事例紹介
私の場合、下記のように、文字列に特定の文字が含まれるかどうかを判断するときにループ内で test
を呼び出していました。
const regexp = /[-#&]/g // 特殊文字
for (const target of values) {
if (regexp.test(target)) {
// 🆖 特殊文字が含まれていても通らないときがある
}
}
指定した文字が含まれていても if 文を通らないことがあるため、バグになっていました。
解決策
解決策は 2 つです。
g
(global) フラグを外す"文字列".match()
に変える
1 が最もシンプルでしょう。仕様のとおり、「グローバルフラグを持つ正規表現」の挙動なのですから、グローバルフラグを付けなければいいのです。余分なものを付けてしまったのがそもそもの間違いです😂
const regexp = /[-#&]/ // 特殊文字for (const target of values) {
if (regexp.test(target)) {
// 👌 特殊文字が含まれているときに通る
}
}
2 は少し表現が変わりますが、これでも所望の挙動が得られます。
for (const target of values) {
if (target.match(/[-#&]/g)) { // g の有無はあまり関係ない // 👌 特殊文字が含まれているときに通る
}
}
おそらく実行速度は 1 のほうが速いでしょう。
まとめ
今回は JavaScript のグローバルフラグを付けた正規表現で、 test
関数や exec
関数を使った場合、予期せぬ挙動になることがあることを紹介しました。
g
1 つで大幅に挙動が変わってしまい、発見も難しいので、正規表現のフラグには気を付けましょう👍
どなたかのお役に立てれば幸いです。