本文首发于我的博客。
标准这事儿吧……
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)
。(但是根据 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 支持就好,哈哈哈哈哈哈。