前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
首页程序员面试题库正则 lastIndex 手动重置 g与y标志
JavaScript数据处理

带 g 的正则连续调用 test,为什么结果一会儿 true 一会儿 false?

带 g 或 y 的正则对象身上有个 lastIndex,test 和 exec 会从这个位置开始找,找到后把它挪到匹配末尾。复用同一个正则去校验别的字符串,就会从错误的位置开始。校验场景直接去掉 g,非要复用就在每次调用前写 re.lastIndex = 0。

前端进阶之旅 · 一题精讲更新于 2026.09.29
JavaScript#数据处理#测试
先看核心答案读代码示例
理解线索

正则对象自带一个读取位置

  1. lastIndex下一次 test 或 exec 从哪个下标开始找
  2. g 标志从 lastIndex 往后搜,成功后更新位置,失败归零
  3. y 标志必须正好在 lastIndex 处匹配,同样会更新位置

match 和 replace 配合 g 时每次都从 0 开始,不受上一次影响;真正会踩坑的是 test 和 exec。

核心回答

先记住这个答案

带 g 或 y 标志的正则是有状态的,它用 lastIndex 记住下一次从哪里开始找。test 和 exec 每次匹配成功,会把 lastIndex 挪到这次匹配的末尾;匹配失败则归零。所以同一个带 g 的正则拿去校验多个字符串,第二次可能从中间开始找,结果就出错了。修法有三种:校验场景直接去掉 g;必须复用时在每次调用前把 lastIndex 设为 0;或者每次调用都新建一个正则。

  • 只有带 g 或 y 时,test 和 exec 才读写 lastIndex
  • 匹配成功挪到匹配末尾,失败归零
  • 校验用的正则不要加 g
  • exec 循环只在开始前归零,循环里不要重置

线上常见的坑:模块级正则做校验

很多人会把正则提到模块顶部当常量,顺手加个 g。单测只测一次看不出问题,上线后连续调用就开始时好时坏。

同一个正则校验三个字符串JavaScript
const hasDigit = /\d/g // 模块级常量,多写了一个 g

console.log(hasDigit.test('a1'), hasDigit.lastIndex) // 命中下标 1,lastIndex 挪到 2
console.log(hasDigit.test('b2'), hasDigit.lastIndex) // 从下标 2 开始找,'b2' 已经到头
console.log(hasDigit.test('c3'), hasDigit.lastIndex) // 失败后归零,又从 0 开始
查看输出与解释
true 2
false 0
true 2

三个字符串都有数字,第二个却返回 false。原因是第一次 test 命中了 'a1' 下标 1 的数字,lastIndex 被改成 2。第二次去测 'b2',引擎从下标 2 开始找,这个位置已经是字符串末尾,自然找不到。失败后 lastIndex 归零,所以第三次又对了。 这就是「true、false 交替」的来历。它不是随机的,而是正则对象上一次调用留下的状态影响了下一次。可以把 lastIndex 理解成书签:g 模式下正则每次都从书签处接着读,换了一本书它也不知道,照样翻到上次那一页。

三种修法怎么选

判断标准很简单:你到底需不需要「从上次的位置接着找」。表单校验、判断字符串里有没有某个字符,都只需要一次性的结果,根本不该带 g。

必须复用时,在入口处归零JavaScript
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 每轮往后挪才能一个个找下去。这时候的规则是:一轮遍历开始前归零,遍历过程中不要碰它。

能匹配空串的正则要手动推进JavaScript
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 就行。

从一道题,走向一组知识

把知识连起来

数据处理

有了 matchAll,为什么不再推荐用 while 循环调 exec?

matchAll 会复制一份正则再遍历,不动原实例的 lastIndex,是手写 exec 循环的替代方案。

数据处理

正则里的 \p{Script=Han} 是什么,怎么用它匹配汉字?

\p{} 需要配合 u 标志,和 g、y 一样是正则标志,可以一起理解标志对匹配行为的影响。

参考资料

  • JavaScript:数据处理参考资料

示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。

本题目录
  1. 先记住这个答案
  2. 线上常见的坑:模块级正则做校验
  3. 三种修法怎么选
  4. exec 循环:在开始前归零,循环里别动
  5. 容易答错的地方
  6. 面试官还会怎么问
  7. 把知识连起来
读懂,再试着讲出来

先看核心答案,再读代码。最后展开追问,检查自己有没有遗漏边界。

试着回答追问
浏览全部面试题理解原理,也关注真实的使用场景。回到顶部 ↑