先记住这个答案
/y(sticky)要求正则引擎只在 lastIndex 指示的索引处尝试匹配,成功则更新 lastIndex 到匹配末尾,失败立即将 lastIndex 置为0,绝不向后跳过。/g(global)允许从 lastIndex 位置开始向后搜索直到找到匹配,成功同样更新,失败则重置为0。两者核心区别在于能否跳过不匹配的字符。工程上,需要连续消费文本时用 /y,全文查找时用 /g。
/y匹配必须从lastIndex起始位置开始/g可从lastIndex向后扫描找匹配- 成功都更新
lastIndex,失败时/y和/g都重置为0
执行流程与 lastIndex 更新
带 g 的正则调用 exec() 时,引擎从 lastIndex 开始向后检索,直到找到匹配或到达末尾。找到则 lastIndex 更新为匹配末尾索引,找不到则重置为0。/y 则强制从 lastIndex 位置原地尝试匹配,若该位置开始的子串不满足模式,立即返回 null 并把 lastIndex 置0,不进行任何扫描。
因此 /y 相当于给模式增加动态锚点,但锚点是可变的 lastIndex,不是 ^。例如 /a/y 在字符串 "ba" 上首次 exec 时,lastIndex 为0,字符 b 不匹配,立即失败;而 /a/g 会跳过 b 在索引1成功。这种差异源于匹配起点是否允许偏移。
词法分析中的连续标记提取
假设解析代码如 const1foo,希望顺序提取标识符,同时不允许遗漏任何非字母字符。用 /[a-z]+/y 时,首次匹配得到 const,此后 lastIndex 指向字符 1 的位置,再次 exec 会因该位置不是字母而失败并将 lastIndex 重置为0,因而能检测到 1 的存在,触发报错或专门的数字处理。若误用 /g,引擎会跳过 1 直接找到 foo,导致 1 被静默忽略,遗漏了本应处理的字符,这就是 sticky 相对 global 在需要严格边界时更有价值的原因。
实际处理时,先令 lastIndex=0,循环调用 exec;返回 null 且 lastIndex 被重置为0,说明当前位置非法,需要报错或推进位置。sticky 模式确保匹配必须连续,这正是手写分词器所需的核心约束,避免意外跳过非法字符。
容易混淆的边界与失效条件
同时使用 /gy 时, /y 的起点锁定优先, g 本身允许继续扫描的语义被覆盖,所以仍严格从 lastIndex 开始。 String.prototype.matchAll 强制要求全局标志,若正则只有 /y 会抛 TypeError,需带 /g 才能使用。另外 m 标志的 ^ 只匹配行首,与 /y 无关。
当手动修改 lastIndex 为负数或 NaN 时会被自动转换为0;设为非整数时会先取整;若设为超出字符串长度的值,匹配立即失败并将 lastIndex 重置为0。因此循环中调用 test() 或 exec(),对于带 g 或 y 的正则,失败后 lastIndex 都会被重置为 0,需正确处理以免死循环。
容易答错的地方
- 认为 /y 等同于 /g 加 ^ 锚定
^匹配字符串开头或行首,与lastIndex无关。即使/g配合^,lastIndex大于0时^无法匹配。/y可设置lastIndex为任意位置进行匹配,两者机制不同,不能等价替换。- 以为 /y 成功时不更新 lastIndex
/y匹配成功后同样把lastIndex设为匹配末尾的索引,与/g一致。匹配失败时,/y和/g都会将lastIndex重置为 0(只要使用了g或y标志)。
面试官还会怎么问?
如何手动将 sticky 正则的 lastIndex 重置到特定位置?
直接给属性赋值,例如 regex.lastIndex = 5。lastIndex 会被规范化为非负整数:负数、NaN 或无法转换为数字的值会被当作0,非整数会向零取整,字符串会先转为数字。重置后下一次匹配将从该索引开始。
String.prototype.matchAll 能配合 /y 使用吗?
不能,因为 matchAll 要求正则必须带全局 g 标志,否则抛出 TypeError。即使写了 /gy,只要包含 g 就可以使用,此时 matchAll 返回迭代器,行为与 exec 循环类似。
在循环中使用 test() 与 exec() 对 lastIndex 的影响有区别吗?
两者都遵循相同的 lastIndex 更新规则:成功时更新到尾后,失败时(对于带 g 或 y 的正则)都会将 lastIndex 重置为 0。区别是 exec() 返回详细匹配结果,而 test() 只返回布尔,若只关心成功与否可用,但循环中需注意失败后 lastIndex 已重置。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。