ECMAScript 2018 (ES9) 在 6 月底正式发布,带来了很多新特性。关于 ES7 和 ES8 相关的知识,可以查看这篇文章 ES2016 和 ES2017 学习。目前大部分 ES7 和 ES8 的特性都得到主流浏览器的支持,而 ES9 的新特性还未能实现很好的兼容性。
关于 ES7/8/9 全部特性可以查看 tc39 官方的 proposals,这些都是最后进入 stage 4 的特性。
ES9 的新特性:
s
(dotAll) flag for regular expressions (正则表达式 dotAll 模式)dotAll 是一个新的正则表达式修饰符,目前 JS 拥有的修饰符有:
正则表达式中的 .
用来匹配任何单个字符,但是有 2 个除外:多字节 emoji 字符和行终结符。
let regex = /^.$/
regex.test('😀') // false
通过设置 u 表示 unicode
let regex = /^.$/u
regex.test('😀') // true
行终止符包括
还有一些其它字符,也可以作为一行的开始:
目前 .
只能匹配其中的一部分:
let regex = /./
regex.test('\n') // false
regex.test('\r') // false
regex.test('\u{2028}') // false
regex.test('\u{2029}') // false
regex.test('\v') // true
regex.test('\f') // true
regex.test('\u{0085}') // true
标记 s
表示 dotAll,用来改变 .
不能匹配行终止符的行为:
/hello.world/.test('hello\nworld') // false
/hello.world/s.test('hello\nworld') // true
或者用 \s
来匹配空白符:
/hello.world/.test('hello\nworld') // false
/hello[\s]world/s.test('hello\nworld') // true
dotAll 表示 .
可以匹配任意字符:
const re = /hello.world/s // 等价于 const re = new RegExp('hello.world', 's')
re.test('hello\nworld') // true
re.dotAll // true
re.flags // 's'
捕获组就是把正则表达式中匹配到的内容,保存到内存中以数字编号或者显式命名的数组里,方便后面使用。这种引用既可以在正则表达式内部,也可以是在正则表达式外部。
捕获组有两种形式,一种是普通捕获组,另一种是命名捕获组。
const regex = /(\d{4})-(\d{2})-(\d{2})/
const matchers = regex.exec('2018-07-02')
matchers[0] // 2018-07-02
matchers[1] // 2018
matchers[2] // 07
matchers[3] // 02
使用数字捕获组的一个缺点是对于引用不太直观,以上面的例子,我们很难分清楚哪个组代表的是年,哪个组代表的是月。而命名捕获组就是为了解决这个问题。
ES2018 允许命名捕获组可以使用 (?<name>...)
语法给每个组起一个名字。
const regex = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/
const match = regex.exec('2018-07-02')
console.log(match.groups.day) // '02'
console.log(match.groups.month) // '07'
console.log(match.groups.year) // '2018'
每个捕获组的名字必须唯一,否则会抛出异常。
const regex = /(?<foo>\d)-(?<foo>\d)/
// Uncaught SyntaxError: Invalid regular expression: /(?<foo>\d)-(?<foo>\d)/: Duplicate capture group name
任何匹配失败的命名组都将返回 undefined。
let re = /^(?<optional>\d+)?$/
const matchers = re.exec('')
matchers[0] === ''
matchers.groups.optional === undefined
let re = /^(?<one>.*):(?<two>.*)$/
let {groups: {one, two}} = re.exec('foo:bar')
console.log(`one: ${one}, two: ${two}`) // 输出 one: foo, two: bar
const reDate = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/
const d = '2018-07-02'
console.log(d.replace(reDate, '$<month>-$<day>-$<year>')) // 07-02-2018
String.prototype.replace
第 2 个参数可以接受一个函数。这时 命名捕获组的引用会作为 groups 参数传递进去:
let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
let result = '2018-07-02'.replace(re, (...args) => {
let {day, month, year} = args[args.length - 1]
return `${day}-${month}-${year}`
})
result === '02/07/2018' // true
当需要在正则表达式里面引用命名捕获组时,使用 \k<name>
语法。
let duplicate = /^(?<half>.*).\k<half>$/
duplicate.test('a*b') // false
duplicate.test('a*a') // true
/(?<name>)/
和 /\k<foo>/
只有在命名捕获组中才有意义。如果正则表达式没有命名捕获组,那么 /\k<foo>/
仅仅是字符串字面量 “k” 而已。
/\k<foo>/.test('k<foo>') // true
断言 (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)当前位置后面的字符串应该满足断言,但是并不捕获,在当前的 JavaScript 正则表达式只支持正向断言。
const regex = /li(?=zhen)/
const match1 = regex.exec('lizhen')
console.log(match1[0]) // li
// 如果字符串没有zhen,则无法匹配
const match2 = regex.exec('liming')
console.log(match2) // null
正向否定断言正好相反
const regex = /li(?!zhen)/
const match1 = regex.exec('lizhen')
console.log(match1) // null
const match2 = regex.exec('liming')
console.log(match2[0]) // li
反向断言和正向断言的行为一样,只是方向相反。反向肯定断言使用语法 (?<=...)
。
比如我们想获取所有的人民币金额,但是不获取其它货币(比如美元):
const regex = /(?<=\D)\d+(\.\d*)?/
const match = regex.exec('$123.89')
console.log(match[0]) // 123.89
Unicode 标准为每个符号分配各种属性和属性值,比如希腊字母 π
在 Unicode 中有独特的属性和属性值。目前版本的 ECMAScript 中正则表达式是无法匹配这些 Unicode 的,通常开发人员有两种选择。
(1) 在运行时使用类似于 xregexp 这样的库创建增强的正则表达式:
const regexGreekSymbol = XRegExp('\\p{Greek}', 'A')
regexGreekSymbol.test('π') // true
缺点是 xregexp 是一个运行时依赖,对性能要求较高的 web 应用来说不是很理想。而且其压缩文件 xregexp-all-min.js.gz
也有 35k,并且每当 Unicode 标准更新时,必须要更新 xregexp 才能使用新数据。
(2) 在编译时的时候使用 regenerate 生成正则表达式。
const regenerate = require('regenerate')
const codePoints = require('unicode-9.0.0/Script/Greek/code-points.js')
const set = regenerate(codePoints)
set.toString()
// → '[\u0370-\u0373\u0375-\u0377\u037A-\u037D\u037F\u0384\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03E1\u03F0-\u03FF\u1D26-\u1D2A\u1D5D-\u1D61\u1D66-\u1D6A\u1DBF\u1F00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FC4\u1FC6-\u1FD3\u1FD6-\u1FDB\u1FDD-\u1FEF\u1FF2-\u1FF4\u1FF6-\u1FFE\u2126\uAB65]|\uD800[\uDD40-\uDD8E\uDDA0]|\uD834[\uDE00-\uDE45]'
// Imagine there’s more code here to save this pattern to a file.
虽然这种方法所生成的正则表达式相当大,但是能够得到最佳的运行时性能。最大的缺点是它需要一个构建脚本,每当 Unicode 标准更新时,必须更新生成脚本。
ES2018 中使用 \p{…}
和 \P{…}
进行 Unicode 的属性转义,在正则表达式中使用 u
进行标记。在 \p{…}
内,可以以键值对的方式设置需要匹配的属性,而非具体内容。比如要匹配希腊字母 π
:
const reGreekSymbol = /\p{Script=Greek}/u
reGreekSymbol.test('π') // true
解决了以下几个问题:
ECMAScript 6 中增加了数组的 Rest 解构赋值和 Spread 语法,比如:
var a, b, rest
[a, b, ...rest] = [10, 20, 30, 40, 50]
console.log(a) // 10
console.log(b) // 20
console.log(rest) // [30, 40, 50]
function sum(x, y, z) {
return x + y + z
}
const numbers = [1, 2, 3]
sum(...numbers) // 6
sum.apply(null, numbers) // 6
ES2018 中增加了对象的 Rest 属性和 Spread 语法。
let {x, y, ...z} = {x:1, y:2, a:3, b:4}
x // 1
y // 2
z // {a:3, b: 4}
let n = {x, y, ...z}
n // {x:1, y:2, a:3, b:4}
Promise.prototype.finally 早就有很多实现,以至于我一直都认为它是原生对象的原型属性。常见的实现有:
Promise.resolve(2).finally(() => {}) // will be resolved with 2
Promise.reject(3).finally(() => {}) // will be rejected with 3
关于 JavaScript 的异步循环,我在之前的文章JavaScript 循环与异步有过探索。如今 ECMAScript 中有了对异步迭代的原生支持。
ES6 中引入迭代器来遍历数组,JavaScript 中的迭代器是一个对象,提供 next() 方法,用来返回序列中的下一项,这个方法包含两个属性:done 和 value。
迭代器对象一旦被创建,就可以反复调用 next()。
function makeIterator (array) {
var nextIndex = 0
return {
next: function () {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{done: true}
}
}
}
// 初始化后 next() 方法可以用来依次访问对象中的键值
var it = makeIterator(['a', 'b'])
console.log(it.next().value) // 'a'
console.log(it.next().value) // 'b'
console.log(it.next().done) // true
常见的可迭代对象有:Array、String、TypedArray、Map、Set。这些对象都内置可迭代的对象,在其原型中有一个 Symbol.iterator
方法。
我们也可以定义可迭代对象。
var myIterable = {}
myIterable[Symbol.iterator] = function* () {
yield 1
yield 2
yield 3
}
for (let value of myIterable) {
console.log(value)
}
// 1
// 2
// 3
var [a, b, c] = [...myIterable] // [1, 2, 3]
// a ===1; b === 2; c === 3
Array.from(myIterable) // [1, 2, 3]
当我们定义了可迭代对象后,就可以在 Array.from
、for...of
中使用这个对象。
一个异步迭代器就像一个迭代器,除了它的 next() 方法返回一个 { value, done } 的 promise。如上所述,我们必须返回迭代器结果的 promise,因为在迭代器方法返回时,迭代器的下一个值和 done 状态可能未知。
const myAsyncIterator = {
[Symbol.asyncIterator]: () => {
const items = ['a', 'b', 'c', 'd']
return {
next: () => Promise.resolve({
done: items.length === 0,
value: items.shift()
})
}
}
}
对于异步迭代器,使用 for await of
进行迭代。
(async function () {
for await (const item of myAsyncIterator) {
console.log(item)
}
})()
异步生成器函数与生成器函数类似,但有以下区别:
yield*
的行为以支持异步迭代。async function* myAsyncGenerator() {
let t = 0
const test = function() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(+new Date())
}, 1000)
})
}
try {
while (t++ < 10) {
yield await test()
}
} finally {
console.log('finally')
}
}
async function main() {
for await (const item of myAsyncGenerator()) {
console.log(item)
}
}
main()
函数返回一个异步生成器(async generator)对象,可以用在 for-await-of 语句中使用。