在此前的 《JS 正则表达式 -- 从入门到精分》 一文中, 曾经较完整的介绍过 JavaScript 中正则表达式的用法. 而从 ES6(ES2015) 开始, 借助 Babel 等标志性的工具, JS 的发展似乎也不想重蹈 Flash 时代的无所作为, 走上了每年一个小版本的快车道; 在此过程中, 正则表达式也陆续演化出一些新的特性.
ECMAScript 和 TC39
虽然可能是大家普遍了解的事情, 但这些称呼反复出现, 可能还是需要稍微解释一下先.
ECMA 指的是 "欧洲计算机制造商协会"(European Computer Manufacturers Association); 现在也称为 "ECMA 国际"(ECMA International), 定位为一家国际性会员制度的信息和电信标准组织.
1996 年 11 月, JavaScript 的创造者 Netscape 公司, 决定将 JavaScript 提交给 ECMA, 希望这种语言能够成为国际标准. 次年, ECMA 发布 262 号标准文件 (ECMA-262) 的第一版, 规定了浏览器脚本语言的标准, 并将这种语言称为 ECMAScript (简称 ES).
此后该标准应用广泛, JavaScript,JScript,ActionScript 等都算是 ECMA-262 标准的实现和扩展.
从 1997 年至 2009 年, 陆续发布了 ES1 ~ ES5; 其中 ES4 其实是偏向于 Adobe 的 ActionScript 风格的实践设计的, 但最终也随着 Flash 被市场上其他厂商封杀, 以改动过大为名不了了之, 但其中一些特性被后来的 ES6 继承.
2015 年, 可以说迄今最重要的一个版本 ES6, 也就是 ES2015(ES6 的第一个版本) 发布.
由各个主流浏览器厂商的代表组成 ECMA 第 39 号技术专家委员会(Technical Committee 39, 简称 TC39), 负责制订新的 ECMAScript 标准.
新的语法从提案到变成正式标准, 需要经历五个阶段. 每个阶段的变动都需要由 TC39 委员会批准:
- Stage 0 - Strawman(展示阶段)
- Stage 1 - Proposal(征求意见阶段)
- Stage 2 - Draft(草案阶段)
- Stage 3 - Candidate(候选人阶段)
- Stage 4 - Finished(定案阶段)
以上这几个也就是我们之前使用 Babel 转译工具时会引入 babel-preset-stage-0 等预置方案的原由, 当然随着 Babel 7 的发布, 这些方案都被统一到了 @babel/preset-env 中.
ES6 中的正则表达式特性
以下特性首次在 ES6 中出现:
"粘性" 修饰符 /y
unicode 修饰符 /u
正则表达式对象上的新属性 flags
用构造函数 RegExp() 拷贝正则表达式
"粘性" 修饰符 /y
修饰符 /y 只将正则表达式的每个匹配锚定到前一个匹配的末尾
简单的说, 这主要与正则表达式对象上的 lastIndex 属性有关 -- 其与 /g 或 /y 的搭配, 会产生不同的效果.
在不设置修饰符, 或只设置了 /g 修饰符的情况下, 只要目标字符串 (或上一次匹配的剩余部分) 中存在匹配就可以.
而 /y 修饰符则告知正则表达式, 只能不偏不倚的从字符串的 lastIndex 那个位置去匹配, 这也就是 "粘性, 粘连" 的涵义.
以 exec() 的使用为例:
- // 不设置修饰符
- const re1 = /a/;
- re1.lastIndex = 7; // 设置的十分明确然而并不会有什么用
- const match = re1.exec('haha');
- console.log(match.index); // 1
- console.log(re1.lastIndex); // 7 (没有变化呢)
- // 设置了 `/g` 修饰符
- const re2 = /a/g;
- re2.lastIndex = 2;
- const match = re2.exec('haha');
- console.log(match.index); // 3 (这次被 lastIndex 影响了)
- console.log(re2.lastIndex); // 4 (更新为匹配成功后的下一位了)
- console.log(re2.exec('xaxa')); // null (4 以后就没有再匹配的了)
- // 设置了 `/y` 修饰符
- const re3 = /a/y;
- re3.lastIndex = 2;
- console.log(re3.exec('haha')); // null (在位置 2 并不匹配)
- re3.lastIndex = 3;
- const match = re3.exec('haha');
- console.log(match.index); // 3 (在位置 3 准确匹配了)
- console.log(re3.lastIndex); // 4 (也更新了)
当然, 一般情况下 -- 比如第一次运行匹配, 或不特别设置 lastIndex 时,/y 的功效大抵和 ^ 起始匹配符相同, 因为此时 lastIndex 为 0 :
- const re1 = /^a/g;
- const re2 = /a/y;
- console.log(re1.test('haha')); // false
- console.log(re2.test('haha')); // false
需要注意的是, 如果同时设置了 /g 和 /y, 则只有 /y 会生效.
sticky 属性
与 /y 修饰符相配套, ES6 的正则表达式对象多了 sticky 属性, 表示是否设置了 /y 修饰符:
- var r = /hello\d/y;
- r.sticky // true
unicode 修饰符 /u
这里简单解释一下 Unicode, 其目标是为世界上每一个字符提供唯一标识符, 该唯一标识符可称为 码点(code point) 或 字符编码(character encode).
在 ES6 之前, JS 的字符串以 16 位字符编码 (UTF-16) 为基础. 每个 16 位序列 (相当于 2 个字节) 是一个编码单元 (code unit, 可简称为码元), 用于表示一个字符. 字符串所有的属性与方法(如 length 属性与 charAt() 方法等) 都是基于这样 16 位的序列.
本来 JS 允许采用 \uxxxx 形式表示一个常用的 unicode 字符, 其中的 4 个十六进制数字表示字符的 unicode 码点:
console.log("\u0061"); // "a"
同时, 这种表示法只能表示码点局限于 0x0000~0xFFFF 之间的字符. 超出这个范围的字符, 必须用两个码元连接的形式 (称为 surrogate-pairs, 代理对) 表示一个码点:
console.log("\uD842\uDFB7"); // ""
这就导致了一个问题, 对于一些超出 16 位 0xFFFF 的 unicode 字符, 传统的方法就会出错; 比如直接在 \u20BB7,JS 会理解成 \u20BB + 7; 所以会显示成一个特殊字符, 后面跟着一个 7.
对此 ES6 做出了改进, 将 unicode 编码放入大括号(这种语法称为 unicode 码点转义符), 就可以正确解读字符了:
console.log("\u{20BB7}"); // ""
同样的例子:
- '\u{1F680}' === '\uD83D\uDE80' //true
- console.log('\u{1F680}') //
- console.log('\uD83D\uDE80') //
此中的转换对应关系, 这篇文章 ( blog.csdn.NET/hherima/art... ) 做了比较清楚的探寻, 感兴趣的话可以结合文末的资料进行研究; 本文不展开掰哧, 能体会示例即可.
书归正传, 在 ES6 的正则中:
修饰符 /u 将正则表达式切换为特殊的 Unicode 模式
在 Unicode 模式下, 既可以使用新的大括号 unicode 编码点转义符表示范围更大的字符, 也可以继续使用 UTF-16 码元. 该模式具有如下特征:
"单独代理"(lone surrogates)特性:
- // 传统的非 Unicode 模式
- /\uD83D/.test('\uD83D\uDC2A') //true, 按 16 位码元识别
- //Unicode 模式
- /\uD83D/u.test('\uD83D\uDC2A') //false, 此模式下会识别成码点中的原子部分
- /\uD83D/u.test('\uD83D \uD83D\uDC2A') //true
- /\uD83D/u.test('\uD83D\uDC2A \uD83D') //true
可以将码点放入正则的字符类中:
- /^[\uD83D\uDC2A]$/.test('\uD83D\uDC2A') //false
- /^[\uD83D\uDC2A]$/u.test('\uD83D\uDC2A') //true
- /^[\uD83D\uDC2A]$/.test('\uD83D') //true
- /^[\uD83D\uDC2A]$/u.test('\uD83D') //false
点操作符匹配码点, 而非码元
- '\uD83D\uDE80'.match(/./gu).length //1
- '\uD83D\uDE80'.match(/./g).length //2
数量描述符也同样匹配到码点
- /\uD83D\uDE80{
- 2
- }/u.test('\uD83D\uDE80\uD83D\uDE80') //true
- /\uD83D\uDE80{
- 2
- }/.test('\uD83D\uDE80\uD83D\uDE80') //false
- /\uD83D\uDE80{
- 2
- }/.test('\uD83D\uDE80\uDE80') //true
正则表达式对象上的新属性 flags
新增的 flags 属性, 会返回正则表达式的修饰符
- const re = /abc/ig;
- console.log( re.source ); //'abc'
- console.log( re.flags ); //'gi'
用 RegExp() 拷贝正则表达式
正则表达式构造函数的传统签名是 new RegExp(pattern : string, flags = ''), 比如:
- const re1 = new RegExp("^a\d{3}", 'gi')
- // 等同于: /^ad{
- 3
- }/gi
ES6 中, 新增的用法是 new RegExp(regex : RegExp, flags = regex.flags):
- var re2 = new RegExp(re1, "yi")
- // 结果是: /^ad{
- 3
- }/iy
这就提供了一种拷贝已有正则表达式, 或更改其修饰符的方法.
ES2018/ES2019 中的新特性
在 ES2018 - ES2019 中, 又增加了一些特性:
命名捕获组
反向引用
反向断言
unicode 属性转义
dotAll 修饰符 /s
命名捕获组
此前的正则表达式操作中, 采用的是 "编号捕获组"(Numbered capture groups)匹配字符串并将之分组, 比如:
- const RE_DATE = /([0-9]{
- 4
- })-([0-9]{
- 2
- })-([0-9]{
- 2
- })/; // 小括号的顺序决定了其编号
- const matchObj = RE_DATE.exec('1999-12-31');
- const year = matchObj[1]; // 1999
- const month = matchObj[2]; // 12
- const day = matchObj[3]; // 31
这种方式无论从易用性还是复杂度方面都不太理想, 尤其当字段过多, 存在嵌套等情况时; 如果改动了正则表达式还容易忘记同步改变分散在各处的编号.
ES6 带来的 "命名捕获组"(Named capture groups), 则可以通过名称来识别捕获的分组
其格式如 (?<year>[0-9]{4})
通过捕获结果中的 groups.year 属性取出
任何匹配失败的命名组都将返回 undefined
- const RE_DATE = /(?<year>[0-9]{
- 4
- })-(?<month>[0-9]{
- 2
- })-(?<day>[0-9]{
- 2
- })/;
- const matchObj = RE_DATE.exec('1999-12-31');
- const year = matchObj.groups.year; // 1999
- const month = matchObj.groups.month; // 12
- const day = matchObj.groups.day; // 31
- // 编号的方式同时被保留
- const year2 = matchObj[1]; // 1999
- const month2 = matchObj[2]; // 12
- const day2 = matchObj[3]; // 31
这还为 replace() 方法提供了额外的便利, 注意其语法:
- const RE_DATE = /(?<year>[0-9]{
- 4
- })-(?<month>[0-9]{
- 2
- })-(?<day>[0-9]{
- 2
- })/;
- console.log( '2018-04-30'.replace(RE_DATE, '$<month>-$<day>-$<year>') ); //04-30-2018
- // 编号捕获组的写法:
- console.log( '2018-04-30'.replace(RE_DATE, "$2-$3-$1") ); //04-30-2018
反向引用(Backreferences)
正则表达式中的 \k<name> 表示这样的意思: 根据前一次匹配到的命名捕获组中的名称, 匹配相应的字符串, 比如:
- const RE_TWICE = /^(?<Word>[a-z]+)!\k<Word>$/;
- RE_TWICE.test('abc!abc'); // true
- RE_TWICE.test('abc!ab'); // false
这种称为反向引用的语法, 对于编号捕获组同样适用:
- const RE_TWICE = /^(?<Word>[a-z]+)!\1$/;
- RE_TWICE.test('abc!abc'); // true
- RE_TWICE.test('abc!ab'); // false
两掺儿的, 也没问题:
- const RE_TWICE = /^(?<Word>[a-z]+)!\k<Word>!\1$/;
- RE_TWICE.test('abc!abc!abc'); // true
- RE_TWICE.test('abc!abc!ab'); // false
反向断言(lookbehind assertions)
根据之前文章的介绍, JS 中已经支持了 "正向断言"(Lookahead assertions), 或称为正向查找.
x(?=y) 匹配'x'仅仅当'x'后面跟着'y'. 称为正向肯定查找
x(?!y) 匹配'x'仅仅当'x'后面不跟着'y'. 称为正向否定查找
ES2018 引入了反向断言(lookbehind assertions), 与正向断言的工作方式相同, 只是方向相反
同样也分为两种子类型:
y(?<=x) 匹配'x'仅仅当'x'前面挨着'y'. 称为反向肯定查找
- // 传统的做法:
- const RE_DOLLAR_PREFIX = /(\$)foo/g;
- '$foo %foo foo'.replace(RE_DOLLAR_PREFIX, '$1bar'); // '$bar %foo foo'
- // 用了反向肯定查找的方法:
- const RE_DOLLAR_PREFIX = /(?<=\$)foo/g;
- '$foo %foo foo'.replace(RE_DOLLAR_PREFIX, 'bar'); // '$bar %foo foo'
y(?<!x) 匹配'x'仅仅当'x'前面不挨着'y'. 称为反向否定查找
- // 传统的做法:
- const RE_NO_DOLLAR_PREFIX = /([^\$])foo/g
- '$foo %foo *foo'.replace(RE_NO_DOLLAR_PREFIX, '$1bar'); //"$foo %bar *bar"
- // 用了反向肯定查找的方法:
- const RE_NO_DOLLAR_PREFIX = /(?<!\$)foo/g;
- '$foo %foo foo'.replace(RE_NO_DOLLAR_PREFIX, 'bar'); // '$foo %bar bar'
unicode 属性转义
在 ES6 的 /u 修饰符基础上, ES2018 添加了 "unicode 属性转义"(Unicode property escapes) -- 形式为 \p{...} 及 \P{...}, 分别表示 "包含" 和 "不包含"
从目的和形式上这很类似于用 \s 来匹配空格等 whitespace, 而 \p{} 和 \P{} 花括号中的部分称为 "unicode 字符属性"(Unicode character properties), 让正则表达式有了更好的可读性.
- /^\p{
- Script=Greek
- }+$/u.test('μετά') //true, 匹配希腊字母, prop=value 的形式
- /^\p{
- White_Space
- }+$/u.test('\t \n\r') //true, 匹配所有空格, bin_prop 的形式
所谓 "unicode 字符属性", 是指在 Unicode 标准中, 每个字符都有用于描述其性质的元数据: properties, 比如:
Name: 一个唯一的名称, 由大写字母, 数字, 连字符, 空格组成, 如:
- A: Name = LATIN CAPITAL LETTER A
- : Name = GRINNING FACE
General_Category: 分类的字符, 如:
- x: General_Category = Lowercase_Letter
- $: General_Category = Currency_Symbol
White_Space: 用于标记不可见的空格, 制表符, 换行等字符, 如:
\t: White_Space = True
π: White_Space = False
Age: Unicode 标准的版本号, 如:
€: Age = 2.1
Block: 码点的一个连续范围, 不会重复, 命名也是唯一的, 如:
- S: Block = Basic_Latin (range U+0000..U+007F)
- : Block = Emoticons (range U+1F600..U+1F64F)
Script: 一个字符集合, 用于一个或多个书写系统
某些 script 支持多个书写系统, 比如 Latin script 支持 English, French, German, Latin 等
某些语言可以用由多种 script 支持的多种替代书写系统书写. 例如, 土耳其语在 20 世纪早期转变为 Latin script 之前就使用了 Arabic script.
举例来说:
α: Script = Greek
Д: Script = Cyrillic
另外几个例子:
- "AaBbCcDD".split("").filter(letter=>{
- return /\p{Lower}/u.test(letter);
- }).join("") //"abc"
- const regex = /^\p{Number}+$/u;
- regex.test('²³¾ ⅠⅡ'); //true
- /\p{Currency_Symbol}+/u.test("¥$€"); //true
dotAll 修饰符 /s
我们通常会在很多正则表达式中见到一种 [\s\S] 的匹配小技巧, 这种看似多余的写法其实是为了弥补 . 标记无法在多行的情况下实现正确匹配的缺憾.
修饰符 /s 解决了这个问题, 所以也称为 dotAll 修饰符.
- console.log(/hello.world/.test('hello\nworld')); // false
- console.log(/hello[\s\S]world/.test('hello\nworld')); // true
- console.log(/hello.world/s.test('hello\nworld')); // true
关于 . 标记, 顺便一提的是:
- /^.$/.test('') //false, 并不将 emoji 识别为一个字符
- /^.$/u.test('') //true, 通过 u 修正
《JS 正则表达式 -- 从入门到精分》
- http://exploringjs.com/es2018-es2019/toc.html
- http://www.appui.org/2496.html
- http://www.cnblogs.com/detanx/p/es6zz.html
- http://www.cnblogs.com/xiaohuochai/p/7230328.html
- http://caibaojian.com/es6/
- https://zhuanlan.zhihu.com/p/27762556
- https://babeljs.io/docs/en/babel-preset-env
- --End--
来源: https://juejin.im/post/5bf7a737e51d450d54417a27