JS 正则在多次 test() 时为什么会出现 lastIndex 缓存问题?

在 JavaScript 中,很多开发者第一次遇到这个问题时都会感觉很奇怪:

javascript 复制代码
const reg = /a/g

console.log(reg.test('a')) // true
console.log(reg.test('a')) // false
console.log(reg.test('a')) // true
console.log(reg.test('a')) // false

明明字符串始终是 "a",为什么结果会一会 true,一会 false

这其实和 JavaScript 正则对象内部的一个属性有关:

复制代码
lastIndex

很多线上 Bug、Vue/React 表单校验异常、Node.js 日志过滤错误,本质上都和它有关。

本文会从:

  • 概念
  • 原理
  • 底层执行机制
  • g / y 标志影响
  • test() / exec() 行为差异
  • 常见坑
  • 实战解决方案
  • 源码级思维

多个层次彻底讲透这个问题。


一、什么是 lastIndex

每一个 JavaScript 正则对象都带有一个隐藏状态:

javascript 复制代码
RegExp.lastIndex

它表示:

下一次正则匹配开始的位置

例如:

javascript 复制代码
const reg = /a/g

console.log(reg.lastIndex)

输出:

复制代码
0

表示:

  • 下一次匹配从索引 0 开始。

二、为什么只有部分正则会出现这个问题?

注意:

不是所有正则都会缓存 lastIndex

只有带:

复制代码
g

或者:

复制代码
y

标志的正则才会。

例如:

bash 复制代码
/a/g
/a/y

而:

bash 复制代码
/a/

不会记录状态。


三、复现最经典的 test()

示例

javascript 复制代码
const reg = /a/g

console.log(reg.test('a'))
console.log(reg.lastIndex)

console.log(reg.test('a'))
console.log(reg.lastIndex)

输出:

arduino 复制代码
true
1

false
0

四、到底发生了什么?

我们一步一步拆解。


第一次执行

arduino 复制代码
reg.test('a')

等价于:

ini 复制代码
从 lastIndex = 0 开始匹配

字符串:

css 复制代码
a

匹配成功:

css 复制代码
索引 0 找到 a

于是:

ini 复制代码
lastIndex = 匹配结束位置

变成:

复制代码
1

所以:

arduino 复制代码
console.log(reg.lastIndex)

输出:

复制代码
1

第二次执行

再次执行:

arduino 复制代码
reg.test('a')

这次:

ini 复制代码
从 lastIndex = 1 开始匹配

但字符串:

css 复制代码
a

长度只有 1。

于是:

复制代码
已经越界

匹配失败。

根据规范:

复制代码
匹配失败时
lastIndex 重置为 0

因此:

arduino 复制代码
false
0

五、底层执行流程(核心)

真正的执行逻辑可以理解为:

arduino 复制代码
function fakeTest(reg, str) {
  // 1. 从 lastIndex 开始匹配
  const start = reg.lastIndex

  // 2. 尝试匹配
  const matched = match(str, start)

  // 3. 成功
  if (matched) {
    reg.lastIndex = matched.end
    return true
  }

  // 4. 失败
  reg.lastIndex = 0
  return false
}

这就是问题根源。


六、为什么 JS 要这么设计?

这是为了支持:

"连续匹配"

例如:

ini 复制代码
const reg = /\d+/g
const str = '123 abc 456 def 789'

如果没有 lastIndex

每次都会从头扫描。

效率会很低。


lastIndex

sql 复制代码
let result

while ((result = reg.exec(str))) {
  console.log(result[0])
}

输出:

复制代码
123
456
789

因为:

每次都会从上一次结束的位置继续。


七、exec() 才是 lastIndex 的真正设计目标

很多人不知道:

scss 复制代码
test()

其实只是:

scss 复制代码
exec() 的简化版

真正依赖 lastIndex 的核心 API 是:

scss 复制代码
exec()

例如:

ini 复制代码
const reg = /\d+/g
const str = '1 22 333'

let match

while ((match = reg.exec(str))) {
  console.log(match[0], reg.lastIndex)
}

输出:

复制代码
1 1
22 4
333 8

八、为什么 test() 最容易踩坑?

因为:

很多人会误以为:

scss 复制代码
test() 是纯函数

即:

复制代码
输入一样
输出一定一样

但实际上:

scss 复制代码
带 g 的 test() 是有状态的

状态就是:

复制代码
lastIndex

九、最危险的真实业务场景

1. 表单校验

错误写法

javascript 复制代码
const reg = /^\d+$/g

function validate(value) {
  return reg.test(value)
}

结果:

scss 复制代码
validate('123') // true
validate('123') // false
validate('123') // true

线上非常常见。


为什么?

因为:

ruby 复制代码
^\d+$

虽然是完整匹配。

但:

复制代码
g

仍然会修改:

复制代码
lastIndex

正确写法

方案 1:不要加 g

javascript 复制代码
const reg = /^\d+$/

这是最推荐方案。


方案 2:手动重置

ini 复制代码
reg.lastIndex = 0

例如:

javascript 复制代码
function validate(value) {
  reg.lastIndex = 0
  return reg.test(value)
}

方案 3:每次创建新正则

javascript 复制代码
function validate(value) {
  return /^\d+$/.test(value)
}

但频繁创建对象有开销。


十、gy 的区别

很多人只知道:

复制代码
g

但:

复制代码
y

其实更特殊。


g:全局匹配

特点:

复制代码
从 lastIndex 开始
向后继续搜索

即使当前位置不匹配:

也会继续往后找。


示例

javascript 复制代码
const reg = /a/g

reg.lastIndex = 1

console.log(reg.test('ba'))

输出:

arduino 复制代码
true

因为:

会继续向后找到:

css 复制代码
索引 1 的 a

y:粘连匹配(Sticky)

特点:

复制代码
必须严格从 lastIndex 开始匹配

不能跳跃。


示例

javascript 复制代码
const reg = /a/y

reg.lastIndex = 1

console.log(reg.test('ba'))

输出:

arduino 复制代码
true

因为:

索引 1 正好是:

css 复制代码
a

但:

arduino 复制代码
reg.lastIndex = 0

console.log(reg.test('ba'))

输出:

arduino 复制代码
false

因为:

索引 0 是:

css 复制代码
b

y 不会继续向后搜索。


十一、哪些 API 会受 lastIndex 影响?

会影响的

API 是否受影响
test()
exec()

不会影响的(内部会重置)

API 是否安全
match() 相对安全
matchAll() 安全
replace() 安全
split() 安全

但:

内部仍然可能使用 lastIndex

只是 JS 引擎帮你处理了。


十二、最容易误解的一点

很多人以为:

scss 复制代码
只有 exec() 才会改 lastIndex

实际上:

scss 复制代码
test() 同样会

这是因为:

ECMAScript 规范中:

javascript 复制代码
RegExp.prototype.test

底层其实调用:

复制代码
RegExpExec

也就是:

scss 复制代码
exec()

十三、源码级理解(V8 思维)

JavaScript 引擎内部:

正则对象不是:

复制代码
无状态对象

而是:

复制代码
带状态机的对象

其中:

复制代码
lastIndex

就是状态机游标。


类似于文件读取指针

例如:

arduino 复制代码
文件读取:
cursor -> 当前读取位置

正则:

rust 复制代码
lastIndex -> 当前扫描位置

本质一样。


十四、为什么很多框架源码会避免复用正则?

例如:

  • Vue
  • React
  • Babel
  • ESLint

很多源码里:

javascript 复制代码
const reg = new RegExp(...)

会频繁创建。

原因之一:

就是避免:

复制代码
lastIndex 状态污染

十五、面试高频题

问题

下面输出什么?

javascript 复制代码
const reg = /foo/g

console.log(reg.test('foo'))
console.log(reg.test('foo'))

答案:

arduino 复制代码
true
false

原因:

ini 复制代码
第一次成功后:
lastIndex = 3

第二次从索引 3 开始匹配
失败
重置为 0

十六、如何彻底避免这个坑?

最佳实践(非常重要)


1. 校验类正则不要加 g

错误:

bash 复制代码
/^\w+$/g

正确:

ruby 复制代码
/^\w+$/

2. 循环提取才使用 g

例如:

python 复制代码
while (reg.exec(str))

3. 复用正则前重置

ini 复制代码
reg.lastIndex = 0

4. 不要把带 g 的正则当纯函数

错误思维:

dart 复制代码
相同输入 => 相同输出

实际上:

dart 复制代码
状态不同 => 输出不同

十七、一个超级经典的线上 Bug

javascript 复制代码
const hasNumber = /\d/g

export function check(str) {
  return hasNumber.test(str)
}

某些用户:

复制代码
偶现校验失败

原因:

多个请求共享:

javascript 复制代码
同一个 RegExp 对象

导致:

复制代码
lastIndex 被污染

修复方案

javascript 复制代码
export function check(str) {
  return /\d/.test(str)
}

或者:

ini 复制代码
hasNumber.lastIndex = 0

十八、总结(核心记忆)

一句话记忆

bash 复制代码
带 g/y 的正则是"有状态"的
状态就是 lastIndex

核心规律

情况 是否修改 lastIndex
普通正则
带 g
带 y

test() 行为本质

scss 复制代码
test() ≈ exec() != null

因此:

复制代码
同样会修改 lastIndex

最重要结论

不要给"校验型正则"加 g

这是所有问题根源。


推荐阅读方式(进阶)

如果你继续深入研究:

建议下一步学习:

  1. RegExp 引擎回溯机制
  2. DFA/NFA
  3. 贪婪匹配
  4. 惰性匹配
  5. 零宽断言
  6. sticky 模式
  7. Unicode 正则
  8. V8 Irregexp 引擎

当你理解这些后:

你会真正掌握 JavaScript 正则底层原理。


本文部分内容借助 AI 辅助生成,并由作者整理审核。

相关推荐
IT_陈寒1 小时前
为什么 Java 的 Optional 让我调试到深夜?
前端·人工智能·后端
米丘2 小时前
React 19.x 的 lazy 与 Suspense
前端·javascript·react.js
如果超人不会飞2 小时前
TinyVue Grid 表格 fetchData 完全指南:从入门到精通
前端
kyriewen2 小时前
手写虚拟DOM后,我反问面试官:key为什么不能用index?
前端·react.js·面试
Doris_20232 小时前
说一说ESLint+Prettier生效的原理
前端·设计模式·架构
ZC跨境爬虫2 小时前
跟着 MDN 学CSS day_21:(图像溢出控制与表单元素样式定制)
前端·javascript·css·ui·交互
卷帘依旧3 小时前
微前端解决方案-qiankun
前端
moshuying3 小时前
你做的,比汇报出来的多得多
前端
shuye2163 小时前
google chrome 离线下载地址
前端·chrome