Skip to content
雲里
里雾

RegExp g flag 的 lastIndex 陷阱

mindgym 开发 更新于 2026/4/23

JavaScript 中带 g(global)flag 的正则表达式是有状态的:每次调用 .test().exec() 后,lastIndex 属性会移动到上次匹配的末尾。如果对不同字符串连续调用同一个全局正则的 .test()lastIndex 不会自动重置,导致匹配结果不可预测。


问题机制

const RE = /\{\{(\w+)\}\}/g

RE.test('hello {{name}}')  // true, lastIndex = 14
RE.test('hi {{age}}')      // false! lastIndex 14 > 字符串长度 10,直接返回 false
RE.test('hi {{age}}')      // true, 上一次 false 后 lastIndex 重置为 0

关键:.test() 匹配失败时会把 lastIndex 重置为 0,但匹配成功后 lastIndex 停在匹配末尾。这意味着交替成功/失败的模式会产生看似随机的结果。

MindGym 中的实际 bug

i18n/useStrings.ts 中用全局正则检测翻译字符串是否含插值参数 {{key}}

const INTERPOLATION_RE = /\{\{(\w+)\}\}/g

// 遍历 bundle 的每个 key
for (const [key, value] of Object.entries(strings)) {
  if (INTERPOLATION_RE.test(value)) {  // ← lastIndex 在迭代间累积
    resolved[key] = (params) => value.replace(INTERPOLATION_RE, ...)
  } else {
    resolved[key] = value
  }
}

zh 的 instruction: '按 1 到 {{max}} 的顺序点击' 匹配成功后 lastIndex = 17,下一次对 en 的 instruction: 'Click numbers 1 to {{max}} in order' 调用 .test() 时从位置 17 开始搜索,恰好跳过了 {{max}}

修复方式

在每次 .test() 前手动重置:

INTERPOLATION_RE.lastIndex = 0
if (INTERPOLATION_RE.test(value)) { ... }

或者不用 g flag(如果只需要检测是否存在而不需要全局匹配):

const HAS_INTERPOLATION = /\{\{(\w+)\}\}/  // 无 g flag,无状态

何时需要 g flag

如果同一个正则既用于 .test() 又用于 .replace(),要么用两个不同的正则实例,要么在 .test() 前 reset lastIndex

参见

参考