
同一个 /a/g 连续调用 test('aba'),结果不是每次都从字符串开头重新找。第一次从位置 0 命中后,lastIndex 变成 1;下一次从位置 1 继续,越过中间的 b,在位置 2 命中。再调用就失败,并把索引归零。于是下一次又能从头命中。
这类代码容易藏在循环条件里,也会让测试用例之间互相影响。我更愿意把正则状态看成输入的一部分,而不是把它当作无副作用的查询对象。
为什么同一个 test 会变
g 表示全局匹配,y 表示粘连匹配。对这两类正则,test() 和 exec() 都会读取 lastIndex 作为开始位置,并在找到匹配时把它更新到匹配结束处。g 可以从该位置之后继续搜索;y 只能要求匹配紧贴该位置。它们都不是每次自动从零开始的开关。lastIndex 是正则对象自己的可写状态,换一段输入文本却继续用同一个对象,并不会自动清掉旧索引。新文本长度更短时,起点甚至可能已经越过文本末尾,然后失败并将索引归零。
没有匹配时,两种标志都会把 lastIndex 归零。这个回绕是循环出现 true 的原因,并非字符串内容变了。完整脚本在 tools/verify-regexp-lastindex-20261017.mjs;在本机 Node.js 中用固定输入逐次运行,输出如下:
global test:
call 1: true, lastIndex=1
call 2: true, lastIndex=3
call 3: false, lastIndex=0
call 4: true, lastIndex=1
global exec:
call 1: index=0, match=a, lastIndex=1
call 2: index=2, match=a, lastIndex=3
call 3: null, lastIndex=0
call 4: index=0, match=a, lastIndex=1
sticky test:
call 1: true, lastIndex=1
call 2: false, lastIndex=0
call 3: true, lastIndex=1
safe reuse:
first=true, reset lastIndex=0, after reset=true, fresh=true
plain test:
match=true, lastIndex=2
exec() 的返回值带有本次匹配位置,所以更容易看出它从哪里开始。test() 只给布尔值,却仍然会改同一个正则对象。若正则被放在模块级常量里,两个函数轮流使用它,索引也会跟着共享。
不带 g 或 y 的正则不会把 lastIndex 当成起点。把它设为 2 再调用 test('aba'),仍从字符串开头命中,索引也保留为 2。仅仅在正则上看到一个非零 lastIndex,还不能断定下一次匹配会从那里开始;要看它是否带全局或粘连标志。
y 不会替你向后找
粘连标志常用于要求输入连续符合某个语法的位置检查。上面的第二次 test 从索引 1 开始,而那里是 b,所以立即失败并重置;第三次调用又从 0 命中。若预期的是从当前游标处接着解析,这正是 y 的用处。若预期在后续任意位置查找,g 的行为不同。
还有一个容易忽略的边界:空字符串匹配可能无法让 lastIndex 向前移动。把这样的表达式直接放进依赖索引推进的循环,循环体就可能反复处理同一个位置。循环里使用 exec() 时,应确认每轮要么结束,要么明确推进游标。
每次从头开始的写法
如果每次判断都应从字符串开头开始,最直接的做法是在调用前执行 regex.lastIndex = 0。若一个正则对象可能被别处共享,创建新的正则实例更能隔离状态。输出中的两种安全做法都再次得到 true。
也可以不加 g 或 y,前提是任务只需要一次从头搜索。不要为了可能之后要找全部就把全局正则顺手塞进普通布尔校验;对很多输入逐个检查时,每次都重新置零,或使用无状态的新实例,结果会更明确。
const pattern = /a/g;
function startsWithA(text) {
pattern.lastIndex = 0;
return pattern.test(text);
}
function startsWithAWithoutSharedState(text) {
return /a/g.test(text);
}
这段代码适用于每次独立判断。若确实要逐个读取所有匹配项,则不要在每次 exec() 前重置,否则扫描会永远回到第一个匹配。状态需要推进时让它推进;状态不该跨调用时,就在边界处重置或换一个实例。
此外,lastIndex 不是所有正则相关方法都同一种方式使用的开关。这里讨论的重点是对同一个 /g 或 /y 实例重复调用 test()、exec()。排查时先看正则是否带这两个标志,再打印调用前后的索引;只看布尔结果,容易把状态推进误认为输入不稳定。