概括

如果你曾用 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. 理解Spark运行模式(一)(Yarn Client)

    Spark运行模式有Local,STANDALONE,YARN,MESOS,KUBERNETES这5种,其中最为常见的是YARN运行模式,它又可分为Client模式和Cluster模式.这里以Spar ...

  2. .Net Core 使用NPOI导入数据

    一.搭建环境 1.新建ASP.NET Core Web 应用程序 2.选择API 3.引用Swashbuckle.AspNetCore NuGet 包进行安装. Swashbuckle.AspNetC ...

  3. MySQL索引长度限制

    索引 TextField是不支持建立索引的 MySQL对索引字段长度有限制 innodb引擎的每个索引列长度限制为767字节(bytes),所有组成索引列的长度和不能大于3072字节 myisam引擎 ...

  4. Android 8.1 自定义热点的时候设置了热点maxLength="32",但是在希伯来语等状态下还是发现在没到32个字符之前就无法把热点设置成功了

    初步认为应该是与热点名称的字节数有关. 然后开始查看源码. /Settings/res/xml/tether_prefs.xml 中的 <Preference android:key=" ...

  5. java的回调和C#的委托

    曾经有人对我说java的回调很巧妙. 今天我自己看了一下,回调的关键就是一个接口的事情. 也许是因为用了一定的手法,一开始不好懂吧,所以看懂了会感觉巧妙. 但是我心里的想法却是,真啰嗦! 回调的实例 ...

  6. springboot+logback日志输出企业实践(下)

    目录 1.引言 2. 输出 logback 状态数据 3. logback 异步输出日志 3.1 异步输出配置 3.2 异步输出原理 4. springboot 多环境下 logback 配置 5. ...

  7. php+redis实现注册、删除、编辑、分页、登录、关注等功能

    本文实例讲述了php+redis实现注册.删除.编辑.分页.登录.关注等功能.分享给大家供大家参考,具体如下: 主要界面 ​ 连接redis redis.php <?php //实例化 $red ...

  8. mui开发:苹果手机自动全屏解决方案

    前一段时间,使用mui写app时,出现了苹果手机播放视频时,自动全屏的情况,并且无法点击控件,只能等到播放完毕后点击控件. 那么怎么解决这个问题呢,弟弟们请看我的下面. 1.在页面的video标签中, ...

  9. 【设计模式大法】Iterator模式

    Iterator模式 --一个一个遍历 在Java中的for语句中 i++的作用是让 i 的值在每次循环后自增1,这样就可以访问数组中的下一个元素.下下一个元素.再下下一个元素,也就实现了从头至尾逐一 ...

  10. QQ登录功能之如何获取用于本地测试的APPID

    本文主要说明一下开发者如何在QQ互联创建测试应用,从而分配给我们一套APP ID和APP KEY,在我们平时学习的时候使用. 一.QQ互联注册开发者 要想使用QQ登陆的功能,首先你必须是腾讯开发者.腾 ...