« 回到博客列表

你可能已经知道的 ES 2018 和 2019

Feb 11th, 2019阅读本文大约需要 11 分钟

标准这事儿吧……

ES 2019(ES 10)标准于年前正式发布,借此机会,我们来看看都有哪些特性有幸转正吧。顺带把 ES 2018 的内容也补一下。

ECMAScript 标准的制定过程,自 2015 年大改,至今已经是第 5 个年头了,想必大家都心里有数了。与 Java 等语言不同,JS 并非先制定标准再开始使用,恰恰相反,是大家先用着,觉得合适的,才收录进标准。标准的存在更像是一个“年度优秀特性合集”。对绝大部分开发者来说,一项特性进没进标准不重要,Babel 支不支持才重要。标准你随便写,不用 Babel 算我输。

那么接下来,我们就来看看 2018 和 2019 两个年度的大合集都有些啥吧。

ES2018(ES9)

1)异步迭代器(Asynchronous Iteration)

总有那么些时候,我们会想要同步执行一些异步的操作,比如下面这样的:

const actions = [
  Promise.resolve(1),
  Promise.resolve(2),
  Promise.resolve(3)
]

利用 async / await 语法,我们可以很轻松的做到这点。

async function process (actions) {
  for (const action of actions) {
    await asyncFunc(action)
  }
}

上面的写法,会按顺序执行 asyncFunc,上一个结束之后才会开始下一个,每次得到的 action 都是一个异步操作本身(比如这里是一个 Promise 对象)。

ES 2018 为我们提供了一种新的方式,在前面代码的基础之上,让每次得到的 action 直接是异步操作完成之后的结果(比如这里是 Promise 被 resolve 之后的结果)。

async function process (actions) {
  for await (const action of actions) {
    asyncFunc(action)
  }
}

2)Rest/Spread Properties 开始适用于对象

这是一个从 ES 2015 开始就被广泛使用的特性,只不过 ES 2015 的标准只支持用于数组,从 ES 2018 开始也支持对象了。

事实上 Map、Set、String 同样支持 ...,但具体是哪个版本引入的我还真没数。(反正我已经用了很久了,不管了)

3)Promise.finally

正如它的名字,finally。这也是个用了好久终于进标准的特性。

在处理 Promise 的返回时,我们经常会遇到这样的情况:无论结果状态是 resolved 还是 rejected,都执行一样的逻辑。

早先遇到这种情况,我们不得不在 then()catch() 里都写一遍,现在可以一次性写在 finally() 里。一个 finally() 就等价于一组回调函数相同的 then()catch()

虽然名字叫“最终”,但并不代表这是 Promise 执行的终点。finally() 后面还可以继续跟 then()catch(),无限跟。

4)移除对“在‘带标签的模版字面量’中使用非法转义序列”的限制

从这里开始的内容比较高阶,一般用不到,赶时间的话你可以跳过,直接去看 ES 2019。

这一节的标题有点绕,我们拆开来讲。首先是“带标签的模版字面量”。

ES 2015 引入了“模板字面量”的特性,相信大家都很熟悉了,长这样:

const name = 'John'
const greetings = `Hi, ${name}` // 'Hi, John'

这个特性有一个生僻用法,它允许我们自定义一个字符串模板函数,比如下面这样:

function myTag(strings, ...params) {
  // strings: ['that ', ' is a ', '']

  const name = params[0]
  const age = params[1]
  const title = age > 99 ? 'centenarian' : 'youngster'

  return strings[0] + name + strings[1] + title
}

const person = 'Mike'
const age = 28
const output = myTag`that ${ person } is a ${ age }`
// that Mike is a youngster

这就是“带标签的模版字面量”。尽管我严重怀疑这个用法的实用性(或许是觉得这样更加语义化?普通函数语义也不差啊?),但 ES 2018 还是选择了对这个特性进行完善。

ES 2016 为这个特性加入了对转义序列的支持,比如八进制(\ 开头)、十六进制(\x 开头)、Unicode 字符(\u 开头),但前提必须是一个有效的转义序列。如果是无效的序列,会报错。

latex`\u00A9`   // 合法,表示“版权符号”
latex`\unicode` // 不合法,报错

ES 2018 去掉了这个限制,主要是考虑到对一些领域特定语言的支持,比如 LaTeX。(学术界一种常用的标记型语言,类似 HTML,其语法会用到大量形如转义序列的指令,如\section\frac\sum 等)

但去掉限制只是说不报错了,模板中的无效转义序列会被替换为 undefined。比如下面这样:

function myTag (template, ...params) {
  console.log({ template, params })
}

const foo = 'foo'
const bar = 'bar'
myTag`aaa${foo}\unicode${bar}bbb`
/* {
  template: ['aaa', undefined, 'bbb', raw: ['aaa', '\unicode', 'bbb]],
  params: ['foo', 'bar']
} */

上面的代码里,template 是模板部分被 ${foo} 等变量分割形成的数组;params 就是 ${foo} 等变量组成的数组。可以看到,\unicode 由于是无效的转义序列,被替换为 undefined,但在 template.raw 里得以保留。

template.raw 是“带标签的模版字面量”中 template 参数特有的一个属性,保存了未被替换的原始字符串。

这样一来,既避免了报错,又保留了开发者自行处理这些转义序列的能力。

5)关于正则表达式的一些改进

5.1)s 标志(dotAll 模式)

在正则表达式中,点号 . 表示匹配任一单个字符,但这不包含换行符(如:\n\r\f 等)。

现在可以通过在尾部增加 s 标志的方式,让它匹配了。

/hello.world/.test('hello\nworld')  // false
/hello.world/s.test('hello\nworld') // true

5.2)扩展 Unicode 匹配范围

一直以来,要编写正则表达式来匹配各种 Unicode 字符并不容易,像 \w\W\d 等都只能匹配英文字符和数字,对于除此之外的字符就很难匹配了,例如非英语的文字。

幸运的是,Unicode 为每个符号添加了元数据属性,并使用它来对各种符号进行分组和描述。例如,Unicode 数据库给所有印地语字符(हिन्दी)设置了 Script 属性,取值为 Devanagari(梵文),还设置了一个 Script_Extensions 属性,同样取值为 Devanagari。我们可以通过搜索 Script=Devanagari 来得到所有印地文字符。

ES 2018 允许正则表达式通过 \p{...} 来扩展 Unicode 符号的匹配范围。例如:

// 扩展匹配范围,允许匹配希腊字符
const reGreekSymbol = /\p{Script=Greek}/u
reGreekSymbol.test('π') // true

// 扩展匹配范围,允许匹配 Emoji
const reEmoji = /\p{Emoji}\p{Emoji_Modifier}/u
reEmoji.test('✌🏽') //true

我们还可以通过 \P{...}(注意,大写 P)来去反,缩小匹配范围。

5.3)正则表达式命名捕获组

正则表达式支持通过括号在一个表达式中指定多个捕获组,就像下面这样:

const
  reDate = /([0-9]{4})-([0-9]{2})-([0-9]{2})/,
  match  = reDate.exec('2019-02-11'),
  year   = match[1], // 2019
  month  = match[2], // 02
  day    = match[3]; // 11

这样的代码虽然可以跑通,但阅读起来比较难懂,而且修改正则有可能会影响到匹配内容的索引。

ES 2018 允许在 ( 后立即使用符号 ?<name> 对捕获组进行命名,匹配失败的会返回 undefined,就像下面这样:

const
  reDate = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/,
  match  = reDate.exec('2019-02-11'),
  year   = match.groups.year,  // 2019
  month  = match.groups.month, // 02
  day    = match.groups.day   // 11

命名捕获组也可以用在 replace() 中,用 $<name> 进行引用(注意,虽然这里的语法和模板字面量很像,但并不是)。例如改变日期格式的顺序:

const
  reDate = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/,
  d      = '2019-02-11',
  usDate = d.replace(reDate, '$<month>-$<day>-$<year>'); // 02-11-2019

5.4)正则表达式的反向断言(lookbehind)

正则表达式支持正向断言(lookahead),例如:

// 正向肯定查找
/x(?=y)/ // 匹配 x,但仅当 x 后面紧跟着 y 时
/Jack(?=Sprat)/.exec('JackSprat') // 'Jack'
/Jack(?=Sprat)/.exec('JackFrost') // null
/Jack(?=Sprat|Frost)/.exec('JackFrost') // 'Jack'

// 正向否定查找
/x(?!y)/ // 匹配 x,但仅当 x 后面不紧跟着 y 时
/Jack(?!Sprat)/.exec('JackSprat') // null
/Jack(?!Sprat)/.exec('Jack Sprat') // 'Jack'

ES 2018 引入了工作方式相同,但是方向相反的反向断言(lookbehind),语法上的差别就在于 ? 变成了 ?<,例如:

// 反向肯定断言
/(?<x)y/ // 匹配 y,但仅当它紧跟在 x 后面时
/(?<=\D)\d+/.exec('$123.89')[0] // 123.89

// 反向否定断言
/(?<!x)y/ // 匹配 y,但仅当它紧跟在 x 后面时
/(?<!\D)\d+/.exec('$123.89')[0] // null

ES 2019(ES 10)

1)JSON 成为 ECMAScript 的完全子集

从学习 JSON 的第一课起,我们就被告知 JSON 应该是专为 JavaScript 而存在的,因此 JSON 是 JavaScript 的子集这一点应该毫无争议啊,这算什么新特性!?

然而细心的开发者却发现,有两个符号是例外:行分隔符(U + 2028)和段分隔符(U + 2029)。在 JSON.parse() 中使用这两个会报语法错误。

ES 2019 把这两个也收入囊中,从今往后,JSON 真正成为 ECMAScript 的完全子集,一个都不少。

2)更友好的 JSON.stringify()

过去,对于一些超出 Unicode 范围的转义序列,JSON.stringify() 会输出未知字符。

JSON.stringify('\uDF06\uD834'); // '"��"'
JSON.stringify('\uDEAD'); // '"�"'

现在,JSON.stringify() 会为其重新转义,显示为有效的 Unicode 序列。

JSON.stringify('\uDF06\uD834'); // '"\\udf06\\ud834"'
JSON.stringify('\uDEAD'); // '"\\udead"'

这和 ES 2018 中对“带标签的模板字面量”的修正,似乎有些许联系。结合历代 ECMAScript 标准,ECMAScript 在处理 Unicode 的问题上着实下了不少功夫。

3)Function.prototpye.toString() 显示更加完善

对一个函数使用 toString() 会返回函数定义的内容。

过去,返回的内容中 function 关键字和函数名之间的注释,以及函数名和参数列表左括号之间的空格,是不会被打出来的。ES 2019 现在回精确返回这些内容,函数怎么定义的,这就就怎么显示。

4)Array.prorptype.flat()Array.prorptype.flatMap()

ES 2019 为数组新增两个函数。

flat() 用于对数组进行降维,它可以接收一个参数,用于指定降多少维,默认为 1。降维最多降到一维。

const array = [1, [2, [3]]]
array.flat() // [1, 2, [3]]
array.flat(1) // [1, 2, [3]],默认降 1 维
array.flat(2) // [1, 2, 3]
array.flat(3) // [1, 2, 3],最多降到一维

flatMap() 允许在对数组进行降维之前,先进行一轮映射,用法和 map() 一样。然后再将映射的结果降低一个维度。可以说 arr.flatMap(fn) 等效于 arr.map(fn).flat(1)。(但是根据 MDNflatMap() 在效率上略胜一筹)

flatMap() 也可以等效为 reduce()concat() 的组合,下面这个案例来自 MDN,但是……这不是一个 map 就能搞定的事么?

var arr1 = [1, 2, 3, 4];

arr1.flatMap(x => [x * 2]);
// 等价于
arr1.reduce((acc, x) => acc.concat([x * 2]), []);
// [2, 4, 6, 8]

flat()flatMap() 都是返回新的数组,原数组不变。

5)String.prototype.trimStart()String.prototype.trimEnd()

ES 2019 为字符串也新增了两个函数:trimStart()trimEnd()。用过 trim() 的朋友都知道了,这两个函数各自负责只去掉单边的多余空格。trim() 是两边都去。

6)Object.fromEntries()

从名字就能看出来,这是 Object.entries() 的逆过程。

7)Symbol.prototype.description

Symbol 是 ES 2015 引入的新的原始类型,通常在创建 Symbol 时我们会附加一段描述。过去,只有把这个 Symbol 转成 String 才能看到这段描述,而且外层还套了个 'Symbol()' 字样。ES 2019 为 Symbol 新增了 description 属性,专门用于查看这段描述。

const sym = Symbol('The description');
String(sym) // 'Symbol(The description)'
sym.description // 'The description'

8)可选的 catch 绑定

try...catch 的语法大家都很熟悉了,过去,catch 后面必须有一组括号,里面用一个变量(通常叫 e 或者 err)代表错误信息对象。现在这部分是可选的了,如果异常处理部分不需要错误信息,我们可以把它省略,像写 if...else 一样写 try...catch

try {
  throw new Error('Some Error')
} catch {
  handleError() // 这里没有用到错误信息,可以省略 catch 后面的 (e)。
}

遗憾

ES 2019 收录了非常多好用的特性,但还是有很多我们非常熟悉,甚至已经用了好久的特性没能进入标准,比如:

  • Stage 3(明年见?)

    • Dynamic Import
    • 私有属性
  • Stage 2(加油?)

    • 装饰器
  • Stage 1(你们慢慢讨论,我们先用为敬)

    • Observable
    • Promise.try
    • String.prototype.replaceAll
    • do

不过这不重要,标准只是官宣,只要 Babel 支持就好,哈哈哈哈哈哈。

该系列的其他文章