ECMAScript 2018( ES2018 ) 新功能 - 正则表达式 - 捕获组命名

yufei       5 年, 9 月 前       828

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'

详情

重叠组名称

正则表达式结果对象已经存在了一些非数字属性,因此可能与捕获组命名发生重叠,例如 lengthindexinput

本提案中,为了捕获组命名和正则表达式结果已经存在的属性重叠,特意将命名组属性放置在单独的 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>),而对于反向引用来说,它通过结果匹配对象的属性提供

目前尚无回复
简单教程 = 简单教程,简单编程
简单教程 是一个关于技术和学习的地方
现在注册
已注册用户请 登入
关于   |   FAQ   |   我们的愿景   |   广告投放   |  博客

  简单教程,简单编程 - IT 入门首选站

Copyright © 2013-2022 简单教程 twle.cn All Rights Reserved.