在 ECMAScript 2018( ES2018 ) 新功能 我们介绍了 TC39 会议已经票选出了 ES2018 的新功能
本着每一小节一个新功能的原则,本小节我们介绍另一个新功能,正则表达式中的 捕获组命名
捕获组命名 - 简介
我们可以给捕获组编号来引用正则表达式匹配的字符串的某些部分,每个捕获组都分配了一个唯一的编号,可以使用该编号进行引用,但这可能使正则表达式难以掌握和重构
如果我们稍后的修改中在中间插入了一个捕获组,那么插入位置之后的所有捕获组的编号都会变化,这样,显然,不是我们所需要的
例如,对于给定的用于匹配日期的正则表达式 /(\d{4})-(\d{2})-(\d{2})/
,如果不检查周边代码的话,我们无法确定哪个捕获组对应于月份,哪一个对应于天
此外,如果给定的日期字符串交换了月份和日期的顺序,那么还应该更新组引用
为捕获组命名就很好的解决了这个问题
高阶 API
可以使用 (?<name>...)
语法为捕获组指定名称,而 name
可以是任意标识符,例如对于我们在简介中提到的日期表达式,可以使用命名捕获组重写为 /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u
,每个名称都应该是唯一的,并遵循 ECMAScript 标识符语法规范
对于命名捕获组结果的访问,我们可以从正则表达式结果的 groups
属性的以捕获组名称为键来访问命名组
需要说明的是,虽然添加了捕获组名称,但同时也会创建对组的编号的引用,就像从来没有使用命名捕获组一样
例如
let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u; let result = re.exec('2015-01-02'); // result.groups.year === '2015'; // result.groups.month === '01'; // result.groups.day === '02'; // result[0] === '2015-01-02'; // result[1] === '2015'; // result[2] === '01'; // result[3] === '02';
对于命名捕获组,可以使用 JavaScript 中的解构来获取它们的值,就像下面这样
let {groups: {one, two}} = /^(?<one>.*):(?<two>.*)$/u.exec('foo:bar'); console.log(`one: ${one}, two: ${two}`); // prints one: foo, two: bar
反向引用
可以通过 \k<name>
语法在正则表达式中访问命名组,例如
let duplicate = /^(?<half>.*).\k<half>$/u; duplicate.test('a*b'); // false duplicate.test('a*a'); // true
命名引用也可以与编号引用同时使用
let triplicate = /^(?<part>.*).\k<part>.\1$/u; triplicate.test('a*a*a'); // true triplicate.test('a*a*b'); // false
替换目标
命名组还可以在 String.prototype.replace
函数的要替换的值的参数中被引用,如果要替换的值是一个字符串,那么可以在该字符串中使用 $<name>
语法来访问命名组,例如
let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u; let result = '2015-01-02'.replace(re, '$<day>/$<month>/$<year>'); // result === '02/01/2015'
需要注意的是,传递给
replace()
函数的字符串必须是一个普通的字符串而不是一个模板字符串,因为该方法将解析day
等的值而不是将它们作为局部变量
另一种方法是字符串模板语法 ${day}
( 但不是模板字符串 ),但提案建议使用 $<day>
与显著区别模板变量
如果传递给 String.prototype.replace()
方法的第二个变量是一个函数,就不能在函数中使用 $<day>
语法,相反,该函数可以接收一个名为 groups
的参数,可以使用该参数来访问命名组,不过,该函数的原型为
function (matched, capture1, ..., captureN, position, S, groups)
可以看到 capture1, ..., captureN
这些参数,因为命名的捕获组仍然会参与编号,跟平常一样,例如
let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u; let result = '2015-01-02'.replace(re, (...args) => { let {day, month, year} = args[args.length - 1]; return `${day}/${month}/${year}`; }); // result === '02/01/2015'
详情
重叠组名称
正则表达式结果对象已经存在了一些非数字属性,因此可能与捕获组命名发生重叠,例如 length
、 index
和 input
本提案中,为了捕获组命名和正则表达式结果已经存在的属性重叠,特意将命名组属性放置在单独的 groups
对象上,该对象是匹配对象的属性
通过单独的 groups
属性,如果将来的 ECMAScript 就可以在 exec()
的结果中添加其它的属性,而不会产生任何 Web 兼容性危险
groups
对象仅在具有命名组的正则表达式结果中创建,而且它不包括编号的组属性,只包括命名的属性
正则表达式中所提到的所有的命名组都会在 groups
中创建一个对等的属性,如果在匹配时没有遇到该命名捕获组,那么它的值就是 undefined
向后兼容新语法
在当前的 ECMAScript
规范中,创建新命名组的语法 /(?<name>)/
会抛出一个语法错误。因此,对于未来的版本,可以将其加入到所有的正则表达式中而不产生任何歧义
但是,命名的反向引用语法 /\k<foo>/
当前在非 Unicode 正则表达式中是允许的,并且与文字字符串 k<foo>
匹配,当然了,在Unicode 正则表达式中,此类转义是禁止的
本提案中,我们将继续维持非 Unicode 正则表达式中的 \k<foo>
匹配文字字符串 k<foo>
的用法,除非正则表达式中存在相关的 foo
命名组
这不会影响现有代码,因为当前没有有效的正则表达式可以具有命名组
这将是一种重构危险,尽管只适用于包含了 \k
代码的正则表达式
其它编程语言中的先例
该提案类似于许多其它编程语言为命名捕获组所做的事情,这似乎是朝着这个方向发展的共识语法
但也存在另类,那就是 Python ,它有一套有趣且引人注目的语法,能够解决非 Unicode 反向引用问题
Perl 引用
Perl 使用了与本提案相同的命名捕获组语法 /(?<name>)/
和反向引用语法 \k<name>
Python 引用
Python 命名组的语法为 (?P<name>)
,反向引用语法为 (?P=name)
Java 引用
JDK7+ 的命名组语法和反向引用语法和本提案相同
.NET 引用
C# 和 VB.NET
支持命名捕获组,语法为 (?<name>)
,同时也支持反向引用,语法为 (?'name')
PHP
根据 Stack Overflow 帖子 和对 php.net 文档 的评论来看,PHP 支持命名组语法很久了,语法格式为 (?P<foo>)
,而对于反向引用来说,它通过结果匹配对象的属性提供