侧边栏壁纸
博主头像
一笑痕

人生若只如初见,
是可喜亦或者是可悲?

  • 累计撰写 241 篇文章
  • 累计收到 7 条评论

全局正则连续 test,小心 lastIndex 留在原处

2026-10-25 / 0 评论 / 3 阅读

全局正则连续 test,小心 lastIndex 留在原处

有段校验代码每次都传入同一个字符串,第一次返回 true,紧接着第二次却变成 false。如果正则带了 g,输入没有变,正则对象却记住了上一次的位置。test() 看起来像一次纯判断,实际会读写 lastIndex。很多时候问题出在把正则放进常量后反复调用:变量没有重新赋值,不代表里面的游标保持原样。

成功一次,下一次从后面接着找

先看 /a/g 对字符串 aa 连续执行。第一次匹配到索引 0,lastIndex 推进到 1;第二次从索引 1 找到另一个 a,索引变成 2。第三次从字符串末尾开始,找不到匹配,返回 false,同时把 lastIndex 归零。

initial lastIndex: 0
test aa #1: true
lastIndex after #1: 1
test aa #2: true
lastIndex after #2: 2
test aa #3: false
lastIndex after #3: 0

这里的状态属于正则实例,不属于某一次调用的临时上下文。相同的正则对象交给另一个调用者时,它会从已有的 lastIndex 接着跑。赋值、共享变量或模块级常量都可能让状态活得比预期更久。

失败会把位置清回零

这个变化不只发生在「字符串里有多个匹配」时。把 lastIndex 手动设为 1,再用 /a/g 测试 ba,查找会从索引 1 开始并命中,接着索引落在 2。若从索引 1 开始测试 ab,此处没有 a,测试失败后索引会归零;下一次测试便重新从开头搜索。

这个归零行为很容易让定位问题变复杂:成功一次后位置前进,失败一次又回到起点。判断某次结果时,不能只看字符串和正则字面量,还要看调用前对象当前的 lastIndex。另外,若 lastIndex 大于字符串长度,全局正则也会失败并复位;不要把它理解为越界后会自动截到末尾。

这点也影响写在帮助函数里的默认行为。假设函数捕获了外层的正则对象,函数每次运行时拿到的仍是同一个实例,调用者就会共享游标。把正则字面量写进函数体,每次调用会创建新实例;不过若需要跨调用累积扫描位置,新实例反而不合适,应该把索引作为明确状态管理。选择哪种写法,要看需求是独立校验还是连续扫描。

共享校验器不是无状态函数

拿 /a/g 校验单字符 a,第一次成功后,索引已到 1。第二次调用从索引 1 开始,字符串长度也只有 1,于是失败并复位。第三次又从 0 开始,因此重新成功。重复校验同一个值,结果会按调用顺序交替。

shared validator first: true
shared validator second: false
shared validator third: true
sticky match at index 1: true
sticky lastIndex after match: 2

我不建议把带 g 的正则直接做成可复用的参数校验器。通常移除 g 就能避免 test() 的全局游标状态;如果逻辑确实需要多次提取匹配,再显式创建新实例,或在每次判断前把 lastIndex 设为 0。重置能解决状态残留,但若多个地方共享同一实例,谁负责重置仍然会成为隐蔽约定。

y 标志也使用 lastIndex

y 是粘连匹配:它要求匹配必须从 lastIndex 指定的位置开始,不会向后搜索。脚本把 /a/y 的索引设为 1,再测 ba,会在位置 1 成功,并把索引推进到 2。它也会修改 lastIndex,但匹配位置要求比 g 更严格。

做词法扫描时,这种「当前位置必须接上」的约束可能有用;普通表单校验通常不需要。带 g 的 test() 若只是想问「字符串是否包含符合模式的片段」,删掉 g 更清楚。需要完整匹配时,应把边界写进正则本身,而不是借助全局标志。

容易记的区分是:g 让搜索从上次结束的位置继续,y 只认当前指定位置。两者的 test() 都会改变 lastIndex。因此调试连续调用时,把正则对象也作为状态来检查;校验独立输入时则优先使用无状态的正则写法。

    🤞 分享