设想一下简单的例子:有字符串 abcd
,我们使用正则表达式 /\w/g
全局匹配它,应该获取到 a
b
c
d
四个字符串。
正则表达式的 exec
方法和字符串的 match
都可以达到这个目的,但使用方式上有差别。
下面的内容只探讨全局匹配模式的情况(也就是正则表达式后面加了 g
标识)。非全局匹配模式,可以基本认为它们没有差别。
1. 返回值的格式不同
match
方法可以一次获取所有结果:
match
的返回值是 RegExpMatchArray | null
,在全局匹配时就是数组。
exec
的返回值则不同:
exec
的返回值有一些额外的属性,可以在需要的时候使用 exec
。
(在非全局匹配时,match
和 exec
的结果一致。)
2. 一次执行,获取的结果数量不同
当我使用 exec
的时候,发现怎么只返回了第一个匹配结果?
exec
的返回值和 match
类似,有结果就是数组,没有结果就是 null。但是 exec
单次匹配只返回一个结果,而非我之前想的,和 match
一样返回所有结果。
3. 运行原理不同(lastIndex)
如果我们要用 exec
获取所有结果,需要把它执行多次;并且需要把正则表达式保存到变量里。像下面这样:
可以看到,exec
每次只返回一个结果。一开始它会从字符串开始的地方进行匹配,之后每次执行都从上次匹配结束的位置继续往下匹配。当这个字符串全部匹配过一次之后,再执行返回的是 null,此时位置会复位到 0,匹配结束。
匹配结束之后再执行的话,又将从头开始匹配,重复这个过程。
上面提到了“位置”,这个位置是保存在正则表达式对象的 lastIndex
属性里的。
你也可以手动修改这个属性,然后让正则表达式从你指定的位置开始匹配。
上面的例子中需要把正则表达式保存到变量里,这样才能储存每次的 lastIndex
。如果不保存到变量,直接执行 /\w/g.exec('abcd')
,每次执行都相当于初始化了表达式 /\w/g
,初始的 lastIndex
为 0,所以这样匹配永远都只能匹配到第一个结果。
上面说了 exec
匹配多个结果需要多次执行,我们可以使用循环来优化这个操作。
这里用了一个变量 result 保存每次执行的结果,以便我们使用。当所有匹配项都查找完毕,最后 exec
会返回一次 null
,循环就结束了。
4. 正则表达式有分组时,结果不同
当正则表达式有分组时,match
会返回整个表达式的匹配,忽略分组。这不就抓瞎了吗?
而 exec
不仅包含整个表达式的匹配,还包括分组结果。
可以看到 exec
里是有两个结果的,这才是我们想要的结果。
而且我本来就是打算将结果 0 替换成 结果 1,也就是 string.replace('[chapter:标题111]', '标题111')
,显然这时候 exec
就很方便了,而 match
无法直接这样替换。
5. exec 要留意字符串的动态变化
exec
执行是一步步执行的,而不是像 match
那样一次执行完毕,所以 exec
需要留意字符串的动态变化。
还是上面的例子:
这个表达式应该能匹配两次,每次都把 chapter 标签换成里面的标题文字。预期结果是 "标题111+标题222"
。
实际上可以看到只替换了第一次,这是为什么呢?
因为代码在执行中动态改变了 str
,导致第二次匹配不到了,第二次结果就是 null
。
还记得 exec
一步步执行靠的是 lastIndex
吗?第一次执行完, lastIndex
是 15,之后 str
变了,前面变短了,现在的 15 已经是 “标题222” 的开始处,导致第二次匹配不到。
解决办法就是把 lastIndex
复位,也就是那一行注释,取消掉注释,每次执行完复位 lastIndex
,这样可以正常匹配到第二处,问题解决了。
这么想的话,其实 exec
里动态修改字符串参数的话,一定要复位 lastIndex
,这样才是最保险的。
6. 使用场景不同
注意只讨论全局匹配的场景。非全局匹配时,它俩似乎没啥区别。
如果觉得选择困难的话,全都用 exec
。因为 exec
显得更全能一些。
详细一点分析的话:
逻辑很简单的,用 match
。
逻辑稍复杂的,用 exec
。
想要一步就得出结果数组的,用 match
。
不需要保存结果数组,直接循环每一个匹配结果,用 exec
。
正则表达式有分组,必须用 exec
。
表面上看起来 match
是最简单的,毕竟一次匹配到了所有的结果:
但实际使用中,获取结果只是第一步,往往还要进行两步操作:
- 判断结果(结果不是 null,才能继续操作)
- 对结果进行循环
这样下来 match
未必占优。如下:
// match const result = "abcd".match(/\w/g) if (result) { for (const str of result) { console.log(str) } } // exec const reg = /\w/g let result while ((result = reg.exec("abcd"))) { console.log(result[0]) }
因为 while
兼具了判断和循环两个步骤,如果需要循环的话,exec
更方便一些。
如果要使用分组的话,就更需要 exec
了。