JS 全局正则的状态陷阱
启用全局或粘性标志后,正则对象会记住「上次匹配到哪里」,下一次匹配从该位置继续;匹配失败时这个位置复位为起点。于是同一段代码对同一个正则对象调用两次,结果可能完全不同。问题往往出现在共享对象上:把带全局标志的正则存成模块常量、类字段或闭包变量,然后在多处调用。
基本信息
| 状态字段 | 正则对象上的位置属性,表示下一次匹配的起始位置 |
|---|---|
| 何时被读写 | 仅在使用全局或粘性标志时,执行方法与测试方法会读写它 |
| 失败行为 | 匹配失败时该位置复位为起始值,下一次又从头部开始 |
| 共享影响 | 同一对象被多处调用时,起点会被上一位调用者改变 |
典型使用场景
- 批量遍历:用循环配合执行方法取出全部匹配时,必须依赖状态推进
- 存在性判断:用测试方法判断字符串是否包含某模式,启用全局标志时结果会随调用次数变化
- 表单校验:同一校验规则被多次调用,若使用共享的全局正则会出现时好时坏的现象
- 并发异步流程:多个异步任务交错使用同一正则对象,状态互相干扰
- 规则热更新:替换正则对象时若沿用旧对象的引用,状态残留会造成疑难问题
状态行为与规避写法
状态行为与规避写法
const g = /a/g;
g.test('aa'); // true,位置推进到 1
g.test('aa'); // true,位置推进到 2
g.test('aa'); // false,位置复位为起点
g.lastIndex = 0; // 需要可重复判定时手动复位
// 共享对象导致现象不稳定:检查函数被多次调用时结果会翻转
function hasA(s: string) { return SHARED_G.test(s) } // 不推荐:共享带状态对象
// 稳妥写法一:判定存在性时用不带全局标志的正则
function hasA2(s: string) { return /a/.test(s) } // 推荐:无状态
// 稳妥写法二:批量遍历时用迭代接口,内部基于副本遍历,不必管理状态
[...'a-1 b-2'.matchAll(/(\w)/g)].map(m => m[1]);写法与状态影响
| 写法 | 是否有状态 | 适用场合 |
|---|---|---|
| 不带全局标志的测试 | 无 | 存在性判断,可重复调用 |
| 带全局标志的测试 | 有 | 需要连续推进时,需自行复位 |
| 执行方法配合循环 | 有 | 手工遍历全部匹配 |
| 迭代接口 | 不影响调用方视角 | 批量遍历,推荐首选 |
| 字符串匹配带全局标志 | 会重置对象状态 | 只取整体匹配时可用 |
空匹配需要特别对待:当模式可以在同一位置匹配出空串时,实现会主动推进位置以避免死循环,因此遍历这类模式时的推进步长与预期可能不同,处理前先确认实际行为。
把正则定义为模块常量本身没有问题,问题在于给它加了全局或粘性标志后又用于纯判断;判断类的用法优先使用无状态模式,需要遍历时使用迭代接口。