在这篇博客文章中,我们将探讨 ECMAScript 2025 特性 ““重复命名捕获组””,该特性由 Kevin Gibbons 提出。
这是一个针对正则表达式的特性,允许我们多次使用相同的捕获组名称。
重复命名捕获组
重复捕获组名称通常不被允许的原因很明显:一个捕获只能有一个值,因此必须忽略其他组——例如:
> /^(?<x>a)(?<x>b)$/
SyntaxError: Invalid regular expression: /^(?<x>a)(?<x>b)$/: 重复捕获组名称
在一个匹配中,组 x
可以捕获 'a'
或 'b'
,但不能同时捕获。
然而,如果重复名称存在于不同的备选项中,那么就没有这样的冲突。以前这也不被允许,但现在被允许了——例如:
> /^((?<x>a)|(?<x>b))$/.exec('a').groups
{ x: 'a' }
> /^((?<x>a)|(?<x>b))$/.exec('b').groups
{ x: 'b' }
这有什么用呢?它允许我们在备选项之间重用正则表达式片段和匹配处理代码。
接下来让我们看一些示例。
用例:具有相似部分的替代格式
函数 parseMonth()
使用正则表达式解析具有两种月份格式之一的字符串:
const {raw} = String;
const {stringify} = JSON;
const RE_MONTH = new RegExp(
raw`^` +
raw`(?<year>[0-9]{4})-(?<month>[0-9]{2})` +
raw`|` +
raw`(?<month>[0-9]{2})\/(?<year>[0-9]{4})` +
raw`$`
);
function parseMonth(monthStr) {
const match = RE_MONTH.exec(monthStr);
if (match === null) {
throw new Error(
'Not a valid month string: ' + stringify(monthStr)
);
}
// 两种备选项相同的代码
return {
year: match.groups.year,
month: match.groups.month,
};
}
assert.deepEqual(
parseMonth('2024-05'),
{ year: '2024', month: '05'}
);
assert.deepEqual(
parseMonth('05/2024'),
{ year: '2024', month: '05'}
);
assert.throws(
() => parseMonth('2024/05')
);
用例:重用正则表达式片段
下面的代码演示了重复命名捕获组如何使我们能够重用正则表达式片段——在这种情况下:KEY
和 VALUE
。
const {raw} = String;
const KEY = raw`(?<key>[a-z]+)`;
const VALUE = raw`(?<value>[a-z]+)`;
const RE_KEY_VALUE_PAIRS = new RegExp(
raw`\(${KEY}=${VALUE}\)` +
raw`|` +
raw`\[${KEY}:${VALUE}\]`,
'g'
);
const str = '[one:a] (two=b)';
const objects = Array.from(
str.matchAll(RE_KEY_VALUE_PAIRS),
// 两种备选项相同的代码
(match) => ({key: match.groups.key, value: match.groups.value})
);
assert.deepEqual(
objects,
[
{ key: 'one', value: 'a' },
{ key: 'two', value: 'b' },
]
);
评论:
string.matchAll()
返回一个可迭代对象。- 我们使用
Array.from()
将该可迭代对象转换为数组。 Array.from()
的可选第二个参数是一个回调函数,它在元素放入返回的数组之前被应用。想想array.map()
。
反向引用
对重复命名组的反向引用按预期工作。以下示例有些牵强(因为我们本可以使用单个命名组),但它说明了可能性:
const RE_DELIMITED = /^((?<delim>\_)|(?<delim>\*))[a-z]+\k<delim>$/;
assert.equal(
RE_DELIMITED.test('_abc_'), true
);
assert.equal(
RE_DELIMITED.test('*abc*'), true
);
assert.equal(
RE_DELIMITED.test('_abc*'), false
);
assert.equal(
RE_DELIMITED.test('*abc_'), false
);
在 JavaScript 引擎中的支持
- 该提案维护了已经支持重复命名捕获组的引擎列表。
- 我使用 Markcheck 测试了这篇博客文章中的代码,并使用了 一个 Babel 插件(因为 Node 的 V8 还不支持该特性)。
- 注意:它只透明支持正则表达式字面量;我使用了 一个变通方法:
结论和进一步阅读
在实践中,重复命名捕获组对于编写基于正则表达式的解析器和分词器的人来说可能是最有用的。对那些人来说,这是一个非常受欢迎的补充。
进一步阅读:
- 我的书 “JavaScript for impatient programmers” 中的 JavaScript 正则表达式章节
- 你可能也会发现我的 用于组合正则表达式的模板标签
re
很有趣。