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
const arrAttr = strTag.match(regAttr);
// [' id="outer"', ' data-a', ' ttt = "asd"']
这时候希望解析上面数组中匹配出来的属性,写出这样一个正则:
const regSplitAttr = /(\s[a-z0-9-_]+\b\s*)(?:=(\s*('|")[\s\S]*?\3))?/ig;
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
?
理论知识
lastIndex
是正则表达式一个可读可写的整形属性,表示下次正则匹配的起始索引。只有在正则本身使用全局匹配 g
时,该属性才会被设置并且起作用。且该属性的设置遵循下面的规则:
- 如果
lastIndex
大于字符串的长度,则regexp.test
和regexp.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 的正则搜索功能。