【JS】380- JavaScript 正则新特性

概括

如果你曾用 JavaScript 进行过复杂的文本处理操作,那么你将会喜欢 ES2018 中引入的新特性。本文将详细介绍第9版标准如何提高 JavaScript 的文本处理能力。

大多数编程语言都支持正则表达式

它们是极其强大的文本处理工具。几十行的文本处理代码通常可以通过一行正则表达式来代替。虽然大多数语言中的内置函数足以对字符串执行搜索和替换操作,但更复杂的操作(例如验证文本输入)通常需要使用正则表达式。

自1999年推出 ECMAScript 标准第三版以来,正则表达式就成为 JavaScript 语言的一部分。ECMAScript 2018(简称 ES2018)是该标准的第九版,引入四个新特性进一步提高了 JavaScript 的文本处理能力:

  • 后行断言
  • 具名组匹配
  • s 修饰符:dotAll 模式
  • Unicode 属性类

以下小节详细介绍这些新特性

后行断言

断言能够根据之前或之后的内容匹配一系列字符,丢弃可能不需要的匹配。当需要处理大段字符串并且意外匹配的可能性很高时,这一特性尤为重要。幸运的是大多数正则表达式都支持后行断言和先行断言。

在 ES2018 之前,JavaScript 中只支持先行断言。先行断言指的是,x 只有在 y 前面才匹配。

先行断言有两种:肯定和否定。先行肯定断言的语法是 (?=...)。例如,正则表达式 /Item(?= 10)/Item 在空格和数字10前才匹配:

const re = /Item(?= 10)/;

console.log(re.exec('Item'));
// → null

console.log(re.exec('Item5'));
// → null

console.log(re.exec('Item 5'));
// → null

console.log(re.exec('Item 10'));
// → ["Item", index: 0, input: "Item 10", groups: undefined]

上面代码使用 exec() 方法在字符串中搜索匹配项。如果找到匹配项,则 exec() 返回一个数组,其第一个元素是匹配的字符串。数组中的 index 属性值是匹配字符串的索引, input 属性值是搜索执行的整个字符串。最后,如果在正则表达式中使用了具名组匹配,则保存在 groups 属性。在这种情况下, groups 值为 undefined 是因为没有具名组匹配。

先行否定断言的语法是 (?!...)。先行否定断言指的是,x 只有不在 y 前面才匹配。例如, /Red(?!head)/Red 不在 head 前才匹配:

const re = /Red(?!head)/;

console.log(re.exec('Redhead'));
// → null

console.log(re.exec('Redberry'));
// → ["Red", index: 0, input: "Redberry", groups: undefined]

console.log(re.exec('Redjay'));
// → ["Red", index: 0, input: "Redjay", groups: undefined]

console.log(re.exec('Red'));
// → ["Red", index: 0, input: "Red", groups: undefined]

ES2018 增加后行断言来完善先行断言。后行断言语法 (?<=...) 表示,x 只有在 y 后面才匹配。

假设以欧元为单位检索产品的价格而不匹配欧元符号。使用后行断言会变得很简单:

const re = /(?<=€)d+(.d*)?/;

console.log(re.exec('199'));
// → null

console.log(re.exec('$199'));
// → null

console.log(re.exec('€199'));
// → ["199", undefined, index: 1, input: "€199", groups: undefined]

注意:先行断言和后行断言通常被称为 “lookarounds”。

后行否定断言的语法为 (?<!...),x 只有不在 y 后面才匹配。例如, /(?<!d{3}) meters/,“ meters” 不在三个数字后才匹配:

const re = /(?<!d{3}) meters/;

console.log(re.exec('10 meters'));
// → [" meters", index: 2, input: "10 meters", groups: undefined]

console.log(re.exec('100 meters'));    
// → null

与先行断言一样,也可以连续使用多个后行断言(肯定或否定)来创建更复杂的模式。举个例子:

const re = /(?<=d{2})(?<!35) meters/;

console.log(re.exec('35 meters'));
// → null

console.log(re.exec('meters'));
// → null

console.log(re.exec('4 meters'));
// → null

console.log(re.exec('14 meters'));
// → [" meters", index: 2, input: "14 meters", groups: undefined]

字符串中 meters 在除了35以外的任意两个数字之后才匹配。后行肯定断言确保匹配的字符串前面有两个数字,后行否定断言确保数字不是35。

具名组匹配

正则表达式可以通过将字符封装在括号中对正则表达式的一部分进行分组,可以在内部反向引用匹配组。此外,还可以通过括号提取匹配值进行进一步处理。

以下代码演示如何在字符串中查找.jpg 扩展名的文件名并提取文件名:

const re = /(w+).jpg/;
const str = 'File name: cat.jpg';
const match = re.exec(str);
const fileName = match[1];

// The second element in the resulting array holds the portion of the string that parentheses matched
console.log(match);
// → ["cat.jpg", "cat", index: 11, input: "File name: cat.jpg", groups: undefined]

console.log(fileName);
// → cat

在更复杂的模式中,使用数字索引只会使已经神秘的正则表达式语法更加混乱。假设匹配日期,由于在某些地区日期和月份的位置交换,因此不清楚哪个组指的是月份,哪个组指的是日期:

const re = /(d{4})-(d{2})-(d{2})/;
const match = re.exec('2020-03-04');

console.log(match[0]);    // → 2020-03-04
console.log(match[1]);    // → 2020
console.log(match[2]);    // → 03
console.log(match[3]);    // → 04

ES2018 针对此问题的解决方法是新增更具表现力的具名组匹配,语法为 (?<name>...):

const re = /(?<year>d{4})-(?<month>d{2})-(?<day>d{2})/;
const match = re.exec('2020-03-04');

console.log(match.groups);          // → {year: "2020", month: "03", day: "04"}
console.log(match.groups.year);     // → 2020
console.log(match.groups.month);    // → 03
console.log(match.groups.day);      // → 04

生成的对象可能包含与具名组同名的属性,所以所有具名组都在 groups 对象里。

许多新的和传统的编程语言中都存在类似的结构。例如,Python 使用 (?P<name>) 表示具名组。Perl 支持具名组,语法与 JavaScript 相同(JavaScript 模仿了 Perl 的正则表达式语法)。Java 也使用与 Perl 相同的语法。

除了能够通过 groups 对象引用具名组,还可以使用数字索引 - 类似于常规捕获组:

const re = /(?<year>d{4})-(?<month>d{2})-(?<day>d{2})/;
const match = re.exec('2020-03-04');

console.log(match[0]);    // → 2020-03-04
console.log(match[1]);    // → 2020
console.log(match[2]);    // → 03
console.log(match[3]);    // → 04

新语法也适用于解构赋值:

const re = /(?<year>d{4})-(?<month>d{2})-(?<day>d{2})/;
const [match, year, month, day] = re.exec('2020-03-04');

console.log(match);    // → 2020-03-04
console.log(year);     // → 2020
console.log(month);    // → 03
console.log(day);      // → 04

即使正则表达式中没有具名组, exec 方法返回的结果中也始终创建 groups 对象:

const re = /d+/;
const match = re.exec('123');

console.log('groups' in match);    // → true

如果可选的具名组没有匹配到, groups 对象仍有该具名组属性,但属性值为 undefined

const re = /d+(?<ordinal>st|nd|rd|th)?/;

let match = re.exec('2nd');

console.log('ordinal' in match.groups);    // → true
console.log(match.groups.ordinal);         // → nd

match = re.exec('2');

console.log('ordinal' in match.groups);    // → true
console.log(match.groups.ordinal);         // → undefined

反向引用某个“常规捕获组”,可以在其后使用 的写法。例如,以下代码使用常规组匹配连续重复的两个字母:

console.log(/(ww)/.test('abab'));    // → true

// if the last two letters are not the same 
// as the first two, the match will fail
console.log(/(ww)/.test('abcd'));    // → false

反向引用某个“具名组匹配”,可以使用 /k<name>/ 的写法:

const re = /(?<dup>w+)s+k<dup>/;

const match = re.exec("I'm not lazy, I'm on on energy saving mode");        

console.log(match.index);    // → 18
console.log(match[0]);       // → on on

此正则表达式在句子中查找连续的重复单词。也可以使用 的写法:

const re = /(?<dup>w+)s+/;

const match = re.exec("I'm not lazy, I'm on on energy saving mode");        

console.log(match.index);    // → 18
console.log(match[0]);       // → on on

/k<name>/ 和 两种写法可以同时使用:

const re = /(?<digit>d)::k<digit>/;

const match = re.exec('5:5:5');        

console.log(match[0]);    // → 5:5:5

与使用数字索引常规捕获组类似, replace() 方法第二个参数可以为具名组,表示方法 $<name>。例如:

const str = 'War & Peace';

console.log(str.replace(/(War) & (Peace)/, '$2 & $1'));    
// → Peace & War

console.log(str.replace(/(?<War>War) & (?<Peace>Peace)/, '$<Peace> & $<War>'));    
// → Peace & War

如果 replace() 方法第二个参数是函数,可以用数字索引的方式引用具名组。该函数的第二个参数为第一个组匹配的值,第三个参数为第二个组匹配的值:

const str = 'War & Peace';

const result = str.replace(/(?<War>War) & (?<Peace>Peace)/, function(match, group1, group2, offset, string) {
    return group2 + ' & ' + group1;
});

console.log(result);    // → Peace & War

s 修饰符:dotAll 模式

默认情况下,点( .)元字符匹配除行终止符(换行符( )和回车符( ))之外的任何字符:

console.log(/./.test(''));    // → false
console.log(/./.test(''));    // → false

尽管有这个缺点,JavaScript 开发人员仍然可以通过使用两个相反的字符类来匹配所有字符,例如 [wW],表示匹配字符( w)或非字符( W):

console.log(/[wW]/.test(''));    // → true
console.log(/[wW]/.test(''));    // → true

ES2018 通过引入 s( dotAll) 修饰符来解决这个问题。使用了此修饰符后,它会更改( .)元字符的行为使换行符也被匹配:

console.log(/./s.test(''));    // → true
console.log(/./s.test(''));    // → true

s 修饰符可以使用在所有正则表达式上,且不会改变依赖于点元字符之前的表现。除了 JavaScript 之外,还有许多其他语言,如 Perl 和 PHP 也有 s 修饰符。

Unicode 属性类

ES2015 中引入 Unicode 感知。但是 u 修饰符仍然无法匹配 Unicode 字符。

考虑以下示例:

const str = '?';

console.log(/d/.test(str));     // → false
console.log(/d/u.test(str));    // → false

? 被认为是一个数字,但 d 只能匹配 ASCII [0-9],所以 test() 方法返回 false。因为改变字符组的行为会破坏现有的正则表达式的表现,所以引入一种新的转义序列。

在 ES2018 中,当设置 u 修饰符时, p{...}可以匹配 Unicode 字符。现在要匹配任何 Unicode 数字,只需使用 p{Number},如下所示:

const str = '?';
console.log(/p{Number}/u.test(str));     // → true

要匹配 Unicode 文字字符,使用 p{Alphabetic}

const str = '漢';

console.log(/p{Alphabetic}/u.test(str));     // → true

// the w shorthand cannot match 漢
console.log(/w/u.test(str));    // → false

P{...}p{...} 的反向匹配,匹配任何 p{...} 不符合的字符:

console.log(/P{Number}/u.test('?'));    // → false
console.log(/P{Number}/u.test('漢'));    // → true

console.log(/P{Alphabetic}/u.test('?'));    // → true
console.log(/P{Alphabetic}/u.test('漢'));    // → false

请注意, p{...} 中使用不支持的属性会导致 SyntaxError

console.log(/p{undefined}/u.test('漢'));    // → SyntaxError

兼容性

总结

ES2018 在之前标准上增加正则表达式特性。新特性包括后行断言,具名组匹配,s 修饰符:dotAll 模式,Unicode 属性类。后行断言,x 只有在 y 后面才匹配。与常规捕获组相比,具名组匹配使用更具表现力的语法。 s( dotAll)修饰符改变 .元字符的表现,匹配换行符。最后,Unicode 属性类提供了一种新的转义序列。

在编写复杂正则表达式时,测试正则表达式通常很有好处。一个好的测试工具提供针对字符串测试正则表达式的接口并展示引擎解析每一步。这在理解其他人编写的表达式时很有用。它还可以检测正则表达式中可能出现的语法错误。Regex101 和 RegexBuddy 是两个流行正则表达式测试工具。

本文分享自微信公众号 - 前端自习课(FE-study)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-10-15

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏生信技能树

生信分析人员如何系统入门python(2019更新版)

一般来说,使用 Python 做生信有两种情况:一种是专门分析生物学数据(主要是各种组学),以调包为主,日常工作就是熟悉各种包的文档,写各种脚本串联工具分析流程...

21620
来自专栏GitHubDaily

近 50 年来最具影响力的 10 种编程语言,都是谁发明的?

软件世界中有各种各样的编程语言,每年还会有新的语言出现,最近发布的编程语言就有 Scala、Kotlin、Go 和 Closure,但历史车轮滚滚向前,浪花淘尽...

595110
来自专栏加米谷大数据

大数据分析:数据可视化图形库(1)

在开源世界中,某些库为数据可视化提供了许多可能性,包括图形或网络表示。其他库仅专注于网络图表示。通常,这些库比通用库提供更多的功能。

16830
来自专栏腾讯大讲堂的专栏

程序员如何才算真正的高效编程?

软件工程师为了准备面试,常常需要花费大量时间练习编程问题,同时还需要完善简历。然而,当他们最终拿下创业公司、谷歌、亚马逊或其他公司的工作后,才发现辛辛苦苦积累...

7520
来自专栏北京马哥教育

Linux下删除大量文件

➜ test for i in $(seq 1 500000);do echo text >>$i.txt;done

14620
来自专栏五分钟学算法

5 门可能衰落的编程语言

并不是所有编程语言都能经久不衰。事实上,新一代的开发人员会采用他们认为更容易使用的其他语言或框架,即使是最流行的语言也不可避免地会走向衰落。

13220
来自专栏科研猫

【科研猫·绘图】团团“圆圆”,来个不一样的月饼

值此中秋佳节到来之际,“科研猫”祝大家节日快乐,还精心给大家备了一份送来圆圆的月饼。

13940
来自专栏华章科技

15年来这8门编程语言位置十分稳定

导读:TIOBE 编程语言排行榜 10 月份的榜单已公布,这期的标题比较有趣 —— “Top 8 of the TIOBE index quite stable...

11530
来自专栏DevOps持续集成

使用Minikube部署单节点集群

10950

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励