概括

如果你曾用 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 是两个流行正则表达式测试工具。

原创系列推荐



4. 
5. 
6. 
7. 

回复“加群”与大佬们一起交流学习~

点这,与大家一起分享本文吧~

【JS】380- JavaScript 正则新特性的更多相关文章

  1. Atitit.js模块化 atiImport 的新特性javascript import

    Atitit.js模块化 atiImport 的新特性javascript import 1. 常见的js import规范amd ,cmd ,umd1 1.1. Require更多流行3 2. at ...

  2. Atitit js版本es5 es6新特性

    Atitit js版本es5 es6新特性 Es5( es5 其实就是adobe action script的标准化)1 es6新特性1 Es5( es5 其实就是adobe action scrip ...

  3. atitit.js 各版本 and 新特性跟浏览器支持报告

    atitit.js 各版本 and 新特性跟浏览器支持报告 一个完整的JavaScript实现是由以下3个不同部分组成的 •核心(ECMAScript)--JavaScript的核心ECMAScrip ...

  4. atitit.atiOrm.js v2 q61 版本新特性.docx

    atitit.atiOrm.js v2 q61 版本新特性.docx 1. V1新特性如下1 1.1. V2规划,直接生成sql在js端1 2. Orm设计框架图1 2.1. atiOrm.js的原理 ...

  5. 介绍Ext JS 4.2的新特性的《深入浅出Ext JS》上市

    以用户为中心的时代,应用的界面外观变得越来越重要.然而,很多程序员都缺乏美术功底,要开发出界面美观的应用实属不易.Ext JS的出现,为广大程序员解决了这一难题.它有丰富多彩的界面和强大的功能,是开发 ...

  6. 【译】 Node.js v0.12的新特性 -- Cluster模式采用Round-Robin负载均衡

    原文:https://strongloop.com/strongblog/whats-new-in-node-js-v0-12-cluster-round-robin-load-balancing 本 ...

  7. javascript ES6 新特性之 扩展运算符 三个点 ...

    对于 ES6 新特性中的 ... 可以简单的理解为下面一句话就可以了: 对象中的扩展运算符(...)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中. 作用类似于 Object.assign() ...

  8. JavaScript ECAMScript5 新特性——get/set访问器

    之前对get/set的理解一直有误,觉得get set 是对象属性方法.看了别人的博客也有很多疑问,今天系统的做了很多测试终于弄明白了.(自己通过看书和写demo测试的,如有不对欢迎大家批评指正) g ...

  9. JavaScript ES6 新特性详解

    JavaScript ES6 带来了新的语法和新的强大功能,使您的代码更现代,更易读 const ,  let and var 的区别: const , let 是 ES6 中用于声明变量的新关键字. ...

随机推荐

  1. lqb 基础练习 特殊回文数

    基础练习 特殊回文数 时间限制:1.0s   内存限制:512.0MB     问题描述 123321是一个非常特殊的数,它从左边读和从右边读是一样的. 输入一个正整数n, 编程求所有这样的五位和六位 ...

  2. 【Java】面向对象之封装

    面向对象编程是对客观世界的模拟,客观世界里成员变量都是隐藏在对象内部的,外界无法直接操作和修改.封装可以被认为是一个保护屏障,防止该类的代码和数据被其他类随意访问.要访问该类的数据,必须通过指定的方式 ...

  3. 构建 DNS 主从复制服务器

    一.主节点配置 1.yum install bind -y 安装 DNS 服务 2.vim /etc/named.conf 编辑 DNS 的配置文件 3.named-checkconf 检查配置文件 ...

  4. MachO文件详解--逆向开发

    今天是逆向开发的第5天内容--MachO文件(Mac 和 iOS 平台可执行的文件),在逆向开发中是比较重要的,下面我们着重讲解一下MachO文件的基本内容和使用. 一.MachO概述 1. 概述 M ...

  5. 解密面试中的套路,你都get到了么?

    如果大家有关注一些测试类的公众号或者论坛的话,肯定会发现很多文章都在表示现在行业的寒冬冷潮来了!然后有很多测试行业从业者,或者转行测试行业者都表示:工作好难找,公司跑了千千万,依然拿不到一个offer ...

  6. vscode + platformIO开发stm32f4

    我的电脑环境 win10 vscode 1.36.1 vscode安装插件 安装完这个插件后会提示你安装 platformIOCore,按照提示安装即可.安装过程可能比较缓慢, 可能需要翻墙. 新建项 ...

  7. 天了噜,为什么外链css要放在头部,js要放在尾部?

    我们最开始学前端的时候都会看到教程在处理外部css,js的时候会将css放在header中,js放在body的最后.为什么要这样子处理,今天参考一些资料好好分析下. 为什么外链css为什么要放头部? ...

  8. JDK API1.6常用方法

    一.String类 常见用法 : (String类代表字符串,JAVA程序中的所有字符串字面值(如“abc”)都作为此类的实例实现,字符串是常量,他们的值在创建之后不能更改,字符串缓冲区支持可变的字符 ...

  9. 前端vue如何下载或者导出word文件和excel文件

    前端用vue怎么接收并导出文件 window.location.href = "excel地址" 如果是 get 请求,那直接换成 window.open(url) 就行了 创建一 ...

  10. C # socket 实例

    同步客户端存储示例 下面的示例程序创建连接到服务器的客户端.             客户端使用一个同步套接字生成,因此,客户端应用程序的执行挂起,直到服务器返回响应.  应用程序将字符串发送到服务器 ...