正则表达式居然有状态

1,582 阅读2分钟

Debugger RegExp.exec() 时候发现了一个属性 lastIndex,正则表达式居然是带状态的。

解析字符串中的属性

const strTag = `<div id="outer" data-a ttt  =  "asd'">`;

假如有一个上面这样子的字符串,希望把三个属性匹配出来,这时候会写出一个这样子的正则表达式:

const regAttr = /\s[a-z0-9-_]+\b(\s*=\s*('|")[\s\S]*?\2)?/gi

02.png

const arrAttr = strTag.match(regAttr);
// [' id="outer"', ' data-a', ' ttt  =  "asd"']

这时候希望解析上面数组中匹配出来的属性,写出这样一个正则:

const regSplitAttr = /(\s[a-z0-9-_]+\b\s*)(?:=(\s*('|")[\s\S]*?\3))?/ig;

01.png

for(const attr of arrAttr) {
  const attrBuffer = regSplitAttr.exec(attr);
  console.log(attrBuffer);
}
// [" id="outer"", " id", ""outer"", """, index: 0, input: " id="outer"", groups: undefined]
// null
// [" ttt  =  "asd"", " ttt  ", "  "asd"", """, index: 0, input: " ttt  =  "asd"", groups: undefined]

看到解析结果,问题来了,为什么在第二种情况下结果是 null

理论知识

MDN lastIndex

lastIndex 是正则表达式一个可读可写的整形属性,表示下次正则匹配的起始索引。只有在正则本身使用全局匹配 g 时,该属性才会被设置并且起作用。且该属性的设置遵循下面的规则:

  • 如果 lastIndex 大于字符串的长度,则 regexp.testregexp.exec 将会匹配失败,然后 lastIndex 被设置为 0
  • 如果 lastIndex 等于字符串的长度,且该正则表达式匹配空字符串,则该正则表达式匹配从 lastIndex 开始的字符串
  • 如果 lastIndex 等于字符串的长度,且该正则表达式不匹配空字符串 ,则该正则表达式不匹配字符串,lastIndex 被重置为 0
  • 否则,lastIndex 被设置为紧随最近一次成功匹配的下一个位置。

在多次使用同一个带 g 的正则,其实下一次的匹配是带有上一次匹配的状态的:

for(const attr of arrAttr) {
  console.log(regSplitAttr.lastIndex);
  const attrBuffer = regSplitAttr.exec(attr);
}
// 0
// 11
// 0

也就是从 11 这个上一次匹配结果保存下来的位置去匹配第二条结果,结果就是没有结果。

问题解决方案

我们写正则一般随手就会写一个 g,但其实这样子可能会导致一些意想不到的结果,对于当前的这个案例,可以这么解决:

  • 把正则中的 g 去掉: /(\s[a-z0-9-_]+\b\s*)(?:=(\s*('|")[\s\S]*?\3))?/i
  • 在每次循环之前,手动把 lastIndex 置零:regSplitAttr.lastIndex = 0;
  • 每次循环之前生成一个新的正则

结论

正则表达式其实非常强大,因为我们很多时候是和字符串在打交道,可以把握每次能够练习到正则的机会。例如平时我们就可以用 VSCode 的正则搜索功能。

博客原文引流

www.chenng.cn/archives/