先记住这个答案
matchAll 解决的是「拿到所有匹配,并且每个匹配都要捕获组」这个需求。以前只能写 while ((m = re.exec(str)) !== null),自己维护循环,还要依赖正则对象的 lastIndex 往前推。matchAll 把这件事封装成一个迭代器,每一项和 exec 的返回值结构一样,有 m[1]、m.groups、m.index。它内部会复制一份正则去遍历,所以原正则的 lastIndex 不会被改,不会影响别处的调用。
matchAll返回迭代器,要展开或用for...of取- 每一项都带捕获组、
groups和index - 传正则时必须带
g,否则抛TypeError - 内部复制正则遍历,原实例
lastIndex不变
以前的写法:exec 循环
在 matchAll 出现之前,str.match(/.../g) 只能返回所有匹配的文本,捕获组全丢了。想要每一次匹配的分组,只能手写 exec 循环。
exec 解析查询字符串JavaScriptconst re = /(\w+)=(\d+)/g
const text = 'a=1&b=2'
const pairs = []
let m
// 每次 exec 从 re.lastIndex 开始找,找到后把 lastIndex 往后挪
while ((m = re.exec(text)) !== null) {
pairs.push(m[1] + ':' + m[2])
}
console.log(pairs.join(','), re.lastIndex) // 最后一次失败,lastIndex 归零查看输出与解释
a:1,b:2 0这段代码能用,但有三个地方容易出错。第一,必须先在外面声明 m,把赋值写在 while 条件里,很多代码规范不允许这种写法。第二,正则必须带 g,漏写的话 exec 每次都从 0 开始,永远返回第一个匹配,循环停不下来。第三,循环要跑到 exec 返回 null 才会把 lastIndex 归零,如果中途 break,lastIndex 就停在中间,这个正则下次再用就会从中间开始找。
换成 matchAll:一个 for...of 搞定
matchAll 是 ES2020 加入的。它返回的不是数组,而是一个迭代器,可以直接放进 for...of,也可以用 [...] 或 Array.from 展开成数组。
const text = 'a=1&b=2'
const re = /(?<key>\w+)=(?<val>\d+)/g
for (const m of text.matchAll(re)) {
// 每一项和 exec 的结果一样:完整匹配、index、groups 都在
console.log(m.index, m[0], m.groups.key, m.groups.val)
}查看输出与解释
0 a=1 a 1
4 b=2 b 2不用声明循环变量,不用判断 null,也不用担心死循环。配合命名组,取值时写 m.groups.key 比 m[1] 好读得多,以后在正则前面再加一个分组,这里也不用跟着改下标。
迭代器还有个好处是按需计算。调用 text.matchAll(re) 时并不会立刻把所有匹配都找出来,for...of 走一步才找一个。处理大文本时只想要前几个匹配,直接 break 就行,不会白白找完整篇。
它不改原正则的 lastIndex
matchAll 内部会用原正则的 source 和 flags 新建一个正则对象,遍历时推进的是这个副本的 lastIndex。复制的时候会带上原来的 lastIndex 作为起点。
const re = /\d/g
re.lastIndex = 2 // 起点会被复制到副本上
const digits = [...'1a2b3'.matchAll(re)].map((m) => m[0])
console.log(digits.join(''), re.lastIndex) // 从下标 2 开始找到 2 和 3,原实例还是 2
try {
'1a2'.matchAll(/\d/) // 没有 g
} catch (err) {
console.log(err.name)
}查看输出与解释
23 2
TypeError遍历结束后,原来那个 re 的 lastIndex 还是 2,说明遍历没有碰原实例。这对模块级共享的正则很重要,matchAll 用完不会给下一个调用者留下脏状态。
不带 g 的正则会直接抛 TypeError。这是故意设计的:方法名叫「全部匹配」,传一个只能匹配一次的正则多半是写错了,与其悄悄只返回一项,不如直接报错。如果传的是字符串,比如 str.matchAll('a'),内部会自动用它创建一个带 g 的正则。
容易答错的地方
- 把返回值当数组,直接调
map或读length matchAll返回的是迭代器,没有map、filter、length。要么先[...str.matchAll(re)]展开,要么Array.from(str.matchAll(re), (m) => m[1])在展开时顺手映射。- 把同一个迭代器用两次
- 迭代器走到头就结束了。
const it = str.matchAll(re)之后先展开一次,再展开第二次得到的是空数组。需要多次使用时,要么先存成数组,要么每次重新调用matchAll。
面试官还会怎么问?
match 加 g 和 matchAll 怎么选?
只要所有匹配的文本,比如找出文章里所有的邮箱,str.match(/.../g) 最简洁,返回一个字符串数组。要每个匹配的分组或者位置,比如解析键值对、做高亮需要知道 index,就用 matchAll。注意 match 在没有匹配时返回 null 而不是空数组,直接调 .length 会报错。
只想要第一个匹配,用 matchAll 合适吗?
没必要。只要第一个匹配和它的分组,用不带 g 的 str.match(re) 或 re.exec(str) 就行,结果结构一样。非要用 matchAll 的话,调一次迭代器的 next() 取 value,也不会多找。
replace 的回调和 matchAll 能互相替代吗?
两者都能拿到每一次匹配的分组。replace 的目的是生成新字符串,回调必须返回替换内容;只为了收集数据而用 replace 的回调是在滥用它,意图不清楚。只收集不替换就用 matchAll,要替换就用 replace 或 replaceAll。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。