标准这事儿吧......
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 https://www.latex-project.org/ .(学术界一种常用的标记型语言, 类似 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).(但是根据 MDN,flatMap() 在效率上略胜一筹)
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.ES 2019 为数组新增两个函数.
和
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 支持就好, 哈哈哈哈哈哈.
来源: https://juejin.im/post/5c69106ef265da2db2792441