ES 提案:String.prototype.matchAll
这篇博客文章解释由 Jordan Harband 提出的提案 “String.prototype.matchAll” 是如何工作的。
在了解提案以前,让我们复习现状。
使用正则表达式获取所有匹配
目前,有几种方式获取正则表达式的所有匹配项。
RegExp.prototype.exec() 配合 /g
如果正则表达式带有 /g
标志,使用 .exec()
多次获取匹配项。在最后匹配后,它返回 null
。在那之前,它返回每一个匹配的匹配对象。每一个对象包含包含捕获的子字符串。
在下面的示例中,我们收集了数组匹配中第 1 组的所有捕获:
function collectGroup1(regExp, str) {
const matches = []
while (true) {
const match = regExp.exec(str)
if (match === null) break
// Add capture of group 1 to `matches`
matches.push(match[1])
}
return matches
}
collectGroup1(/"([^"]*)"/gu, `"foo" and "bar" and "baz"`)
// [ 'foo', 'bar', 'baz' ]
没有 /g
标识,.exec()
总是仅仅返回第一次匹配项:
const re = /[abc]/
re.exec('abc')
// -> [ 'a', index: 0, input: 'abc' ]
re.exec('abc')
// -> [ 'a', index: 0, input: 'abc' ]
这对于 collectGroup1()
是坏消息,因为如果 regExp
没有标志 /g
,它将绝不会完成。
String.prototype.match() 配合 /g
如果使用设置有标志 /g
的正则表达式配合 .match()
方法,将得到包含所有匹配项的数组(换句话说,捕获分组被无视):
'abab'.match(/a/gu)
// -> [ 'a', 'a' ]
如果 /g
没有设置,.match()
方法与 RegExp.prototype.exec()
运行结果一样:
'abab'.match(/a/u)
// -> [ 'a', index: 0, input: 'abab' ]
String.prototype.replace() 配合 /g
可以使用技巧通过 .replace()
收集捕获:使用函数计算分割值。这个函数获得所有的捕获信息。然而,通过计算分割值来替代,收集数据很有意思,来看数组匹配:
function collectGroup1(regExp, str) {
const matches = []
function replacementFunc(all, first) {
matches.push(first)
}
str.replace(regExp, replacementFunc)
return matches
}
collectGroup1(/"([^"]*)"/gu, `"foo" and "bar" and "baz"`)
// -> [ 'foo', 'bar', 'baz' ]
正则表达式没有标志 /g
的话,.replace()
方法同样仅访问第一次匹配。
RegExp.prototype.test()
.test()
只要正则表达式匹配就返回 true
:
const regExp = /a/gu
const str = 'aa'
regExp.test(str) // true
regExp.test(str) // true
regExp.test(str) // false
String.prototype.split()
可以使用正则表达式分割器分割字符串。如果正则表达式包含捕获分组,那么 .split()
方法返回分组捕获的子字符串:
const regExp = /<(-+)>/gu
const str = 'a<--->b<->c'
str.split(regExp)
// -> [ 'a', '---', 'b', '-', 'c' ]
这些方法的问题
这些方法有几个不利因素:
- 冗长且不直观
- 只有设置标志
/g
时才作用,有时我们会从其它地方收到正则表达式,例如通过参数。 然后如果我们要确保找到所有匹配项,则必须检查是否设置了此标志。 - 为了跟踪进度,所有方法(
.match()
除外)都会更改正则表达式:.lastIndex
属性记录上一个匹配项的结束位置。 这使得在多个位置使用相同的正则表达式具有风险。尽管通常不建议这样做,但遗憾的是,当多次使用.exec
时,不能内联正则表达式(因为每次调用都会重置正则表达式):// 没有用 const match = /abc/u.exec(str)
- 由于属性
.lastIndex
确定匹配在何处继续,因此当我们开始收集匹配项时,该值必须始终为零。但是,至少.exec()
在最后一次匹配之后将其重置为零。如果不为零,会发生以下情况:const regExp = /a/gu regExp.lastIndex = 2 regExp.exec('aabb') // null
String.prototype.matchAll() 提案
这是如何调用 .matchAll()
:
const matchIterator = str.matchAll(regExp)
给定字符串和正则表达式,.matchAll()
返回包含所有匹配项的配对对象迭代器。
可以使用扩展运算符(...
)转化这个迭代器为数组:
;[...'-a-a-a'.matchAll(/-(a)/gu)]
// -> [ [ '-a', 'a' ], [ '-a', 'a' ], [ '-a', 'a' ] ]
.matchAll()
不关心是否设置标志 /g
:
;[...'-a-a-a'.matchAll(/-(a)/gu)]
// -> [ [ '-a', 'a' ], [ '-a', 'a' ], [ '-a', 'a' ] ]
使用 .matchAll()
,函数 collectGroup1()
变得更短且容易理解:
function collectGroup1(regExp, str) {
const results = []
for (const match of str.matchAll(regExp)) {
results.push(match[1])
}
return results
}
让我们使用扩展运算符和 .map()
使这个函数更简洁:
function collectGroup1(regExp, str) {
const arr = [...str.matchAll(regExp)]
return arr.map(x => x[1])
}
另一个选择是使用 Array.from()
,转化为数组并且同时映射。因此,不再需要中间值 arr
:
function collectGroup1(regExp, str) {
return Array.from(str.matchAll(regExp), x => x[1])
}
.matchAll() 返回一个不可重新开始的迭代器
.matchAll()
返回一个迭代器,并且不可重新开始。因此,一旦结果用尽,需要再次调用该方法并创建一个新的迭代器。
相反,.match()
加 /g
返回一个可迭代的数组,可以根据需要对其进行多次迭代。
实现一个 .matchAll() 方法
这是 matchAll
的实现:
function ensureFlag(flags, flag) {
return flags.includes(flag) ? flags : flags + flag
}
function* matchAll(str, regex) {
const localCopy = new RegExp(regex, ensureFlag(regex.flags, 'g'))
let match
while ((match = localCopy.exec(str))) {
yield match
}
}
进行功能复制需确保以下几点:
/g
被设置。regex.lastIndex
没有改变。localCopy.lastIndex
是零。
使用 matchAll()
:
const str = '"fee" "fi" "fo" "fum"'
const regex = /"([^"]*)"/
for (const match of matchAll(str, regex)) {
console.log(match[1])
}
// 输出:
// fee
// fi
// fo
// fum
FAQ
为什么不是 RegExp.prototype.execAll()?
一方面,.matchAll()
运行就像批量版本的 .exec()
,因此名称 .execAll()
很有意义。
另一方面,exec()
更改正则表达式 match()
则不更改(exec()
与matchAll()
正则表达式相同)。这就解释了为什么选择名称 matchAll()
。
延伸阅读
- “Exploring ES6” 章节 “Iterables and iterators”
- “Exploring ES6” 章节 “The spread operator (…)”
- “Exploring ES6” 章节 “Array.from()”
本文由 吳文俊 翻译,原文地址 ES2020: String.prototype.matchAll