ECMAScript 2018 (ES9) 在 6 月底正式发布,带来了很多新特性。关于 ES7 和 ES8 相关的知识,可以查看这篇文章 ES2016 和 ES2017 学习。目前大部分 ES7 和 ES8 的特性都得到主流浏览器的支持,而 ES9 的新特性还未能实现很好的兼容性。
关于 ES7/8/9 全部特性可以查看 tc39 官方的 proposals,这些都是最后进入 stage 4 的特性。
ES9 的新特性:
- Lifting template literal restriction 模板语法修正
s(dotAll) flag for regular expressions (正则表达式 dotAll 模式)- RegExp named capture groups (正则表达式命名捕获组)
- Rest/Spread Properties (Rest/Spread 属性)
- RegExp Lookbehind Assertions (正则表达式反向(lookbehind)断言)
- RegExp Unicode Property Escapes (正则表达式 Unicode 转义)
- Promise.prototype.finally
- Asynchronous Iteration (异步迭代器)
正则表达式 dotAll 模式
dotAll 是一个新的正则表达式修饰符,目前 JS 拥有的修饰符有:
- g -> global
- i -> ingoreCase
- m -> multiline
- y -> sticky
- u -> unicode
- s -> dotAll
正则表达式中的 . 用来匹配任何单个字符,但是有 2 个除外:多字节 emoji 字符和行终结符。
1 | let regex = /^.$/ |
通过设置 u 表示 unicode
1 | let regex = /^.$/u |
行终止符包括
- U+000A LINE FEED (LF) (\n) - 换行
- U+000D CARRIAGE RETURN (CR) (\r) - 回车
- U+2028 LINE SEPARATOR - 行分隔符
- U+2029 PARAGRAPH SEPARATOR - 段分隔符
还有一些其它字符,也可以作为一行的开始:
- U+000B VERTICAL TAB (\v)
- U+000C FORM FEED (\f)
- U+0085 NEXT LINE
目前 . 只能匹配其中的一部分:
1 | let regex = /./ |
标记 s 表示 dotAll,用来改变 . 不能匹配行终止符的行为:
1 | /hello.world/.test('hello\nworld') // false |
或者用 \s 来匹配空白符:
1 | /hello.world/.test('hello\nworld') // false |
dotAll 表示 . 可以匹配任意字符:
1 | const re = /hello.world/s // 等价于 const re = new RegExp('hello.world', 's') |
正则表达式命名捕获组
捕获组就是把正则表达式中匹配到的内容,保存到内存中以数字编号或者显式命名的数组里,方便后面使用。这种引用既可以在正则表达式内部,也可以是在正则表达式外部。
捕获组有两种形式,一种是普通捕获组,另一种是命名捕获组。
1 | const regex = /(\d{4})-(\d{2})-(\d{2})/ |
使用数字捕获组的一个缺点是对于引用不太直观,以上面的例子,我们很难分清楚哪个组代表的是年,哪个组代表的是月。而命名捕获组就是为了解决这个问题。
命名捕获组
ES2018 允许命名捕获组可以使用 (?<name>...) 语法给每个组起一个名字。
1 | const regex = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/ |
名字唯一
每个捕获组的名字必须唯一,否则会抛出异常。
1 | const regex = /(?<foo>\d)-(?<foo>\d)/ |
匹配失败
任何匹配失败的命名组都将返回 undefined。
1 | let re = /^(?<optional>\d+)?$/ |
使用解构赋值
1 | let re = /^(?<one>.*):(?<two>.*)$/ |
使用 replace
1 | const reDate = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/ |
String.prototype.replace 第 2 个参数可以接受一个函数。这时 命名捕获组的引用会作为 groups 参数传递进去:
1 | let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/ |
反向引用
当需要在正则表达式里面引用命名捕获组时,使用 \k<name> 语法。
1 | let duplicate = /^(?<half>.*).\k<half>$/ |
向下兼容
/(?<name>)/ 和 /\k<foo>/ 只有在命名捕获组中才有意义。如果正则表达式没有命名捕获组,那么 /\k<foo>/ 仅仅是字符串字面量 “k
1 | /\k<foo>/.test('k<foo>') // true |
正则表达式反向(lookbehind)断言
断言 (Assertion) 是一个对当前匹配位置之前或之后的字符的测试,它不会实际消耗任何字符,所以断言也被称为“非消耗性匹配”或“非获取匹配”。
正则表达式的断言一共有 4 种形式:
(?=pattern)零宽正向肯定断言(zero-width positive lookahead assertion)(?!pattern)零宽正向否定断言(zero-width negative lookahead assertion)(?<=pattern)零宽反向肯定断言(zero-width positive lookbehind assertion)(?<!pattern)零宽反向否定断言(zero-width negative lookbehind assertion)
正向断言(lookahead)
当前位置后面的字符串应该满足断言,但是并不捕获,在当前的 JavaScript 正则表达式只支持正向断言。
1 | const regex = /li(?=zhen)/ |
正向否定断言正好相反
1 | const regex = /li(?!zhen)/ |
反向断言(lookbehind)
反向断言和正向断言的行为一样,只是方向相反。反向肯定断言使用语法 (?<=...)。
比如我们想获取所有的人民币金额,但是不获取其它货币(比如美元):
1 | const regex = /(?<=\D)\d+(\.\d*)?/ |
正则表达式 Unicode 转义
Unicode 标准为每个符号分配各种属性和属性值,比如希腊字母 π 在 Unicode 中有独特的属性和属性值。目前版本的 ECMAScript 中正则表达式是无法匹配这些 Unicode 的,通常开发人员有两种选择。
(1) 在运行时使用类似于 xregexp 这样的库创建增强的正则表达式:
1 | const regexGreekSymbol = XRegExp('\\p{Greek}', 'A') |
缺点是 xregexp 是一个运行时依赖,对性能要求较高的 web 应用来说不是很理想。而且其压缩文件 xregexp-all-min.js.gz 也有 35k,并且每当 Unicode 标准更新时,必须要更新 xregexp 才能使用新数据。
(2) 在编译时的时候使用 regenerate 生成正则表达式。
1 | const regenerate = require('regenerate') |
虽然这种方法所生成的正则表达式相当大,但是能够得到最佳的运行时性能。最大的缺点是它需要一个构建脚本,每当 Unicode 标准更新时,必须更新生成脚本。
解决方案
ES2018 中使用 \p{…} 和 \P{…} 进行 Unicode 的属性转义,在正则表达式中使用 u 进行标记。在 \p{…} 内,可以以键值对的方式设置需要匹配的属性,而非具体内容。比如要匹配希腊字母 π:
1 | const reGreekSymbol = /\p{Script=Greek}/u |
解决了以下几个问题:
- 不用为创建 Unicode-aware 正则表达式担心。
- 不需要运行时依赖。
- 正则表达式不需要使用 Unicode 区间来判断特点的内容。
- 不需要生成正则表达式脚本。
- Unicode 属性转义自动保持最新,每当 Unicode 标准更新时,ECMAScript 引擎更新其数据即可。
Rest/Spread 属性
ECMAScript 6 中增加了数组的 Rest 解构赋值和 Spread 语法,比如:
1 | var a, b, rest |
1 | function sum(x, y, z) { |
ES2018 中增加了对象的 Rest 属性和 Spread 语法。
Rest 属性
1 | let {x, y, ...z} = {x:1, y:2, a:3, b:4} |
Spread 语法
1 | let n = {x, y, ...z} |
Promise.prototype.finally
Promise.prototype.finally 早就有很多实现,以至于我一直都认为它是原生对象的原型属性。常见的实现有:
1 | Promise.resolve(2).finally(() => {}) // will be resolved with 2 |
Asynchronous Iteration
关于 JavaScript 的异步循环,我在之前的文章JavaScript 循环与异步有过探索。如今 ECMAScript 中有了对异步迭代的原生支持。
迭代器 Iterator
ES6 中引入迭代器来遍历数组,JavaScript 中的迭代器是一个对象,提供 next() 方法,用来返回序列中的下一项,这个方法包含两个属性:done 和 value。
迭代器对象一旦被创建,就可以反复调用 next()。
1 | function makeIterator (array) { |
可迭代对象
常见的可迭代对象有:Array、String、TypedArray、Map、Set。这些对象都内置可迭代的对象,在其原型中有一个 Symbol.iterator 方法。
我们也可以定义可迭代对象。
1 | var myIterable = {} |
当我们定义了可迭代对象后,就可以在 Array.from、for...of 中使用这个对象。
异步迭代器
一个异步迭代器就像一个迭代器,除了它的 next() 方法返回一个 { value, done } 的 promise。如上所述,我们必须返回迭代器结果的 promise,因为在迭代器方法返回时,迭代器的下一个值和 done 状态可能未知。
1 | const myAsyncIterator = { |
对于异步迭代器,使用 for await of 进行迭代。
1 | (async function () { |
异步生成器函数
异步生成器函数与生成器函数类似,但有以下区别:
- 当被调用时,异步生成器函数返回一个对象:async generator,该对象有 3 个方法(next,throw,和 return),每个方法都返回一个 Promise,Promise 返回 { value, done }。而普通生成器函数并不返回 Promise,而是直接返回 { value, done }。这会自动使返回的异步生成器对象具有异步迭代的功能。
- 允许使用 await 表达式和 for-await-of 语句。
- 修改了
yield*的行为以支持异步迭代。
1 | async function* myAsyncGenerator() { |
函数返回一个异步生成器(async generator)对象,可以用在 for-await-of 语句中使用。