先记住这个答案
带 g 或 y 标志的正则是有状态的,它用 lastIndex 记住下一次从哪里开始找。test 和 exec 每次匹配成功,会把 lastIndex 挪到这次匹配的末尾;匹配失败则归零。所以同一个带 g 的正则拿去校验多个字符串,第二次可能从中间开始找,结果就出错了。修法有三种:校验场景直接去掉 g;必须复用时在每次调用前把 lastIndex 设为 0;或者每次调用都新建一个正则。
- 只有带
g或y时,test和exec才读写lastIndex - 匹配成功挪到匹配末尾,失败归零
- 校验用的正则不要加
g exec循环只在开始前归零,循环里不要重置
三种修法怎么选
判断标准很简单:你到底需不需要「从上次的位置接着找」。表单校验、判断字符串里有没有某个字符,都只需要一次性的结果,根本不该带 g。
const DIGIT = /\d/g
function hasDigit(str) {
DIGIT.lastIndex = 0 // 每次调用前把书签放回开头
return DIGIT.test(str)
}
console.log(hasDigit('a1'), hasDigit('b2'), hasDigit('c3'))查看输出与解释
true true true归零写在函数入口,调用方就不用关心这个正则之前被谁用过。
第一种修法是去掉 g。没有 g 和 y 的正则,test 和 exec 每次都从下标 0 开始,完全不看 lastIndex,你手动给它赋值也没用。这是最推荐的写法,代码也最干净。
第二种就是上面这段,在每次调用前写 re.lastIndex = 0。适合正则还要在别处做遍历、不能去掉 g 的情况。
第三种是在函数里直接写正则字面量,比如 function check(s) { return /\d/g.test(s) }。每次调用都会创建一个新的正则对象,lastIndex 天然是 0。代价是每次都多建一个对象,在高频调用的热路径上不如前两种。
exec 循环:在开始前归零,循环里别动
遍历所有匹配时,g 和 lastIndex 反而是你需要的东西。while ((m = re.exec(text)) !== null) 这种写法,就是靠 lastIndex 每轮往后挪才能一个个找下去。这时候的规则是:一轮遍历开始前归零,遍历过程中不要碰它。
const re = /\d*/g // \d* 能匹配空字符串
const text = 'a1'
const found = []
let m
while ((m = re.exec(text)) !== null) {
found.push(`${m.index}:${m[0]}`)
// 空匹配时 lastIndex 不会前进,不手动 +1 就会死循环
if (m[0] === '') re.lastIndex++
}
console.log(found.join(' '))查看输出与解释
0: 1:1 2:在下标 0,'a' 不是数字,但 \d* 可以匹配 0 个字符,于是得到一个空匹配。空匹配的末尾就是起点,lastIndex 停在 0 不动,下一轮还在同一个位置得到同样的结果,循环永远停不下来。所以要在空匹配时手动让 lastIndex 加 1。
如果在循环体里写 re.lastIndex = 0,效果也是死循环:每轮都回到开头,永远拿到第一个匹配。
容易答错的地方
- 以为所有字符串方法都会被
lastIndex影响 str.match(re)和str.replace(re, ...)配合g时,内部会先把lastIndex设为 0 再开始,结束后也归零,所以不会受之前调用影响。str.search会先存下原来的lastIndex,查完再还原。真正会读上次状态的是test和exec,排查时先看这两个。- 以为给没有
g的正则设lastIndex能控制起点 - 不带
g和y时,exec永远从下标 0 开始,lastIndex属性可以赋值,但引擎不读它。想从某个位置开始找,要么加g,要么先用slice截出后半段。
面试官还会怎么问?
matchAll 会改原正则的 lastIndex 吗?
不会。matchAll 内部会复制一个新的正则对象去遍历,原实例的 lastIndex 保持不变。不过复制时会带上原来的 lastIndex,如果你事先把它设成了 2,遍历就会从下标 2 开始。
g 和 y 在匹配失败时有什么不同?
两者失败后都会把 lastIndex 归零。区别在失败之前:g 会从 lastIndex 开始一路往后找,y 只看 lastIndex 这一个位置,这里对不上就直接失败,不会往后跳。
代码评审时怎么一眼看出这类隐患?
看到带 g 的正则被 test 调用就要警惕。因为 test 只要一个布尔结果,用不上「接着找」的能力,加 g 只会带来状态问题。尤其是模块级常量、React 组件外定义的正则、工具函数里共享的正则,被多处调用时最容易出错,改成不带 g 就行。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。