自 ECMA2015 (6th) 大幅更新之后, ECMA 标准变更成每年6月发布一个版本进行小幅度更新。为方便温习和查找,汇总一下近五年的所有版本特性。本文共涵盖了 ES2016、ES2017、ES2018、ES2019、ES2020 五个版本的更新内容。翻译有删改,仅供快速查找使用。
前言:关于ECMA
ECMA 相关stage-x 处于某个阶段,描述的是 ECMA 标准相关的内容。根据提案划分界限,stage-x 大致分为以下阶段:
- stage-0:还是一个设想,只能由 TC39 成员或 TC39 贡献者提出。
- stage-1:提案阶段,比较正式的提议,只能由 TC39 成员发起,这个提案要解决的问题必须有正式的书面描述。
- stage-2:草案,有了初始规范,必须对功能语法和语义进行正式描述,包括一些实验性的实现。
- stage-3:候选,该提议基本已经实现,需要等待实验验证,用户反馈及验收测试通过。
- stage-4:已完成,必须通过 Test262 验收测试,下一步就纳入 ECMA 标准。
总结起来就是数字越大,越成熟。
ES2016 新特性
ES2016 只更新了两个特性:
- Array.prototype.includes()
- 指数运算符
Array.prototype.includes()
该方法用于检测数组中是否包含某个值,包含则返回 true,否则返回 false。
1 | let array = [1, 2, 4, 5]; |
结合 fromIndex 使用:
可以为 .includes()
提供一个起始索引,默认是 0,接受负数值。
1 | let array = [ 1, 3, 5, 7, 9, 11 ]; |
指数操作符 (**)
在 ES2016 前我们会这样写:
1 | Math.pow(2, 2); |
现在,有了指数运算符之后,可以这样写:
1 | 2 ** 2; |
这在多次操作指数运算的时候很有用:
1 | 2 ** 2 ** 2 |
Math.pow()
需要连续调用,这会使代码看起来很长不宜阅读。使用指数运算符的方式更快更简洁。
ES2017 新特性
ES2017 介绍了更多新特性,如 String padding,Object.entries(), Object.values(), 原子性操作, 以及 Async、Await 等。
字符串填充 ( String.padStart() 和 String.padEnd() )
.padStart()
对字符串头部进行填充, .padEnd()
对字符串尾部进行填充:
1 | "hello".padStart(6); |
为什么只填充1个空格而不是6个?是因为 “hello” 一共是五个字符,而 .padStart
和 .padEnd
的入参是填充后的字符串长度,所以之只会填充一个空格。
使用 padStart 实现文本右对齐
1 | const strings = ["short", "medium length", "very long string"]; |
第一步获取了数组中最长字符串的长度,接下来用该长度填充数组中的每个字符串,即打印出一组右对齐的字符串。
自定义填充值
除了默认的空格,还可以使用字符串和数字进行填充。
1 | "hello".padEnd(13," Alberto"); |
Object.entries() 和 Object.values()
首先创建一个Object:
1 | const family = { |
在上个版本的 javascript 中,我们可以使用如下方式获取 Object 中的值:
1 | Object.keys(family); |
Object.keys()
仅会返回对象中所有的键名。
现在又多了两种可以访问对象的方法:
1 | Object.values(family); |
Object.values()
以数组形式返回对象所有值。Object.entries()
同样以数组形式返回对象中的键值对。
Object.getOwnPropertyDescriptors()
这个方法会返回对象所有自身属性的描述。描述性的字段有:value
,writable
, get
, set
, configurable
和 enumerable
。
1 | const myObj = { |
尾行逗号
这仅仅是语法上的一个小改变。现在在写 Object 属性值时,我们可以在每个值后边加上一个逗号,不论它是否是最后一个。
1 | // from this |
注意上述第二个例子中的最后一个逗号,即使你不写它也不会报错,只是写上会更方便的开发者们协作。
共享内存和原子性操作
下述引自 MDN:
多个共享内存的线程能够同时读写同一位置上的数据。原子操作会确保正在读或写的数据的值是符合预期的,即下一个原子操作一定会在上一个原子操作结束后才会开始,其操作过程不会中断。
这些原子操作属于 Atomics 模块。与一般的全局对象不同,Atomics 不是构造函数,因此不能使用 new 操作符调用,也不能将其当作函数直接调用。Atomics 的所有属性和方法都是静态的(与 Math 对象一样)。
方法示例:
- add / sub
- and / or / xor
- load / store
Atomics
通常和 SharedArrayBuffer
对象(通用的固定长度二进制数据缓冲区)一起使用。
来看一下几个 Atomics
方法的使用示例:
Atomics.add(), Atomics.sub(), Atomics.load(), and Atomics.store()
Atomics.add()
共接受三个参数:array、index、value。并返回该索引在执行操作前的值。
1 | // create a `SharedArrayBuffer` |
要从数组中检索特定的值,可以使用 Atomics.load()
并传递两个参数,一个数组和一个索引。Atomics.sub()
的使用方式与 Atomics.add()
类似,只不过它是减去某个值。
1 | // create a `SharedArrayBuffer` |
上述示例调用 Atomics.sub()
方法,实现 unit8[0] - 5 ,相当于 10 - 5。如同 Atomics.add()
一样,该方法也会返回数组中该索引在执行操作前的值。
使用 Atomics.store()
来存储一个值,使用 Atomics.load()
来加载一个值。
Atomics.and(), Atomics.or(), Atomics.xor()
这三个方法都在数组的给定位置执行按位的 AND、OR 和 XOR 操作。不再赘述。
Async 和 Await
ES2017 提供了两个操作 Promise 的新方法:”async/await”。
回顾一下 Promise
在介绍新语法之前,让我们快速浏览下之前我们是怎么使用 Promise 的:
1 | // fetch a user from github |
上述是一个非常简单的例子:请求一个 Github 用户的数据,并打印。下面来看个复杂点的:
1 | function walk(amount) { |
来看下,如何用新语法 async / await 来重写 Promise
。
Async 和 Await
1 | function walk(amount) { |
让我们来分解一下上述代码都做了什么:
- 创建一个异步函数需要在 function 前面添加 async 关键词
- 这个关键词会告诉 Javascript 返回一个 Promise
- 如果指定 async 函数返回一个非 Promise 的值,那么这个值将会被包含在 Promise 中然后被返回
- 顾名思义, await 会告诉 Javascript 等待 promise 返回结果
错误处理
通常在 promise 中,我们使用 .catch()
捕获最终的错误。现在有一点不同了:
1 | async function asyncFunc() { |
ES2018 新特性
对象扩展运算符
还记得 ES6 中我们可以使用扩展运算符来做什么吗:
1 | const veggie = ["tomato", "cucumber", "beans"]; |
现在,扩展运算符同样适用于对象:
1 | let myObj = { |
使用扩展运算符,我们可以轻松的复制对象(浅复制)。
异步的迭代
使用异步的迭代,我们可以异步的遍历数据。
引自文档
异步迭代器很像迭代器,只不过迭代器的 next 方法返回一对 { value, done }
为此,我们将使用一个 for-await-of
循环,它将迭代转换成 Promise。
1 | const iterables = [1, 2, 3]; |
在执行过程中,[Symbol.asyncIterator]()
方法将会创造一个异步的迭代器,每次访问序列中的下一个值时,我们都会隐式地等待迭代器方法返回 Promise。
Promise.prototype.finally()
引自 MDN:
finally() 方法返回一个 Promise。在 Promise 结束时,无论结果是 fulfilled 或者是 rejected,都会执行指定的回调函数。这为在 Promise 是否成功完成后都需要执行的代码提供了一种方式。避免了同样的语句需要在 then() 和 catch() 中各写一次的情况。
1 | const myPromise = new Promise((resolve, reject) => { |
.finally()
同样会返回一个 promise,所以我们可以继续链式调用 then
和 catch
方法,但是它们是基于之前的 promise 进行调用的。
1 | const myPromise = new Promise((resolve, reject) => { |
从上边代码可以看到 finally
后边的 then
返回的值是由第一个 then
创建的,而不是 finally
。
正则表达式的新特性
在新版的 ECMA 中,共更新了 4 个关于正则的特性。
- 正则表达式的 s (doAll) 标志
- 正则表达式捕获组命名
- 正则表达式反向断言 (Lookbehind Assertions)
- unicode 字符转义 (Unicode property escapes)
s (doAll) 标志
引自MDN
dotAll
属性表明是否在正则表达式中一起使用 “s
“ 修饰符(引入 /s 修饰符,使得.
可以匹配任意单个字符,包括换行符和回车符)
1 | /foo.bar/s.test('foo\nbar'); |
捕获组命名
想要引用正则匹配到的某一部分字符串可以为捕获组编号。每个捕获组的数字都是唯一的,可以对应的数字引用它们,但是这使正则表达式难以阅读和维护。例如
/(\d{4})-(\d{2})-(\d{2})/
匹配一个日期,但如果不看上下文的代码,就无法确定哪一组对应于月份,哪一组是一天。当然,如果哪一天需要交换日期和月份的顺序,那么对应的组引用也需要更新。现在,可以使用(?<name>...)
来为捕获组命名,以表示任何标识符名称。重写上述例子:/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u
每一个命名都是唯一且遵循 ECMA 命名规范的。命名的组可以通过匹配结果的result
属性来访问。对组的数字引用也会被建立,就像未命名的组一样。看下边几个例子:
1 | let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u; |
反向断言
使用反向断言可以确保匹配之前或者之后没有其他匹配。反向断言的语法表示为
(?<=...)
。
例如:匹配一个美元数值且不包含美元符号可以这样写/(?<=$)\d+(\.\d*)?/
,这个表达式会匹配$10.53
并返回10.53
,而并不会匹配€10.53
。而(?<!...)
匹配的规则正相反,它会匹配不存在表达式中的匹配项,例如/(?<!$)\d+(?:\.\d*)/
不会匹配$10.53
,但是会匹配€10.53
。
Unicode 字符转义
Unicode 字符转义是一种新的转义序列,
u
作为字符转义的标志,\p{...}
和\P{...}
用来添加转义符。有了这个特性,匹配 Unicode 字符可以这样写:
1 | const regexGreekSymbol = /\p{Script=Greek}/u; |
解除模板字符限制
当使用 Tagged 模板字符串时,转义字符的限制被移除了(阅读更多)
ES2019 新特性
Array.prototype.flat() / Array.prototype.flatMap()
Array.prototype.flat()
会递归地展平一个数组并作为新值返回,它接受一个表示递归深度的值,未传值则默认深度为1。可以用 Infinity
去展平所有嵌套的数组。
1 | const letters = ['a', 'b', ['c', 'd', ['e', 'f']]]; |
Array.prototype.flatMap()
与深度值为1的 flat 几乎相同,但它并非仅仅展平数组。 flatMap 接收一个处理函数,使用 flatMap()
可以在展平的同时更改对应的值并返回一个新的数组。
1 | let greeting = ["Greetings from", " ", "Vietnam"]; |
这有点类似于 map()
方法,只不过多了一次展平操作。
Object.fromEntries()
Object.fromEntries() 将一组键值对转换成对象。
1 | const keyValueArray = [ |
我们可以将任何可迭代的值作为 Object.entries()
方法的参数,不论它是一个 Array
还是 Map
,或是其他实现了迭代协议的值。
注:可迭代协议( Iteration Protocols )是 ES2015 提出的,通常通过常量 Symbol.iterator
访问该对象的可迭代属性。
1 | var someString = "hi"; |
String.prototype.trimStart() / .trimEnd()
String.prototype.trimStart()
移除字符串前面的空白符,String.prototype.trimEnd()
移除字符串后面的空白符。
1 | let str = " this string has a lot of whitespace "; |
也可以使用 .trimStart()
和 trimEnd()
的别名: .trimLeft()
和 .trimRight()
。
可选的 catch 捕获参数
在 ES2019 之前,你必须为 catch 捕获传递一个表示异常的变量,现在这个变量不是必要的了。
1 | // Before |
这在你想忽略错误参数的时候很有用。
Function.ptototype.toString()
.toString()
方法返回一个代表函数源码的字符串。
1 | function sum(a, b) { |
注释也会被包含其中:
1 | function sum(a, b) { |
Symbol.prototype.description
.description
返回 Symbol
对象可选描述的字符串。
1 | const me = Symbol("Alberto"); |
ES2020 特性
BigInt 类型
BigInt 是 JavaScript 第七个原始类型,它允许开发者操作非常大的整型。
数字类型可以处理 2 ** 53 - 1
即 9007199254740991
以内的数。可以通过常量 MAX_SAFE_INTEGER
来访问这个值。
1 | Number.MAX_SAFE_INTEGER; // 9007199254740991 |
顾名思义,若操作的 number 值超过最大值时,运行结果就会变的奇怪。使用 BigInt
类型则没有明确的界限,因为它的界限取决于运行设备的内存。
定义 BigInt
类型,你即可以通过给 BigInt()
构造函数传递一个字符串值来创建,也可以像平常一样使用字面量语法来创建,但是要在尾部加上一个字符 n
。
1 | const myBigInt = BigInt("999999999999999999999999999999"); |
注意,BigInt
类型与常规类型的数字并不是完全兼容的,这意味这你确定最好仅在操作比较大的数据时使用它。
1 | const bigInt = 1n; // small number, but still of BigInt type |
总之,使用 JS 做比较复杂的数学运算时 BigInt
是个不错的选择。它在替换专门用于处理大量数字的库方面表现良好。现在至少在整型方向有所进展,而目前我们对 BigDecimal
的提案了解的还很少。
动态导入(Dynamic imports)
动态导入,允许在浏览器端动态地加载代码模块。使用 import()
语法来导入你的代码块。
1 | import("module.js").then((module) => { |
import()
返回一个 promise,resolve 中会返回代码模块加载后的内容。可以使用 ES6 的 .then()
方法或者 async/await
来处理加载结果。
空值合并操作符(??)
空值合并操作符(??)是一个新的 JS 运算符,当所访问的值是 null 或者 undefined 时,它会提供一个默认值。
1 | const basicValue = "test"; |
但是这跟 逻辑或(||)有什么区别呢?当第一个数是虚值 (在 Boolean 上下文中认定为 false 的值),如 false
, 0
, 或者""
,以及空值 null
和 undefined
,那么 逻辑或 将会使用第二个操作数。而空值合并操作符仅仅是在第一个值为空值而不是虚值的时候才会使用第二个操作数。如果你的代码可以接受除了 null
和 undefined
以外的任何值,那么空值合并操作符就是最佳选择。
1 | const falseValue = false; |
可选链(?.)
与空值合并操作符类似,只不过可选链是处理 Object 中 null
和 undefined
的。鉴于直接从空值中国获取属性值会报错,现在可选链会直接将空值返回。
1 | const obj = { |
当然,这只是一个语法糖,但也是一个很受欢迎的补充。记住不要在代码里到处使用这些操作符,他们虽然用起来方便,但从性能角度来说,它比普通的 .
开销要大。而且,若是代码是经过 Babel 和 TypeScript 转义的,则更要谨慎使用。
GlobalThis
由于 JavaScript 的代码可以运行在多个不同的环境,例如 浏览器、Node.js、Web Worker 等,要实现这种交叉兼容性绝非易事,globalThis 的出现方便了这些操作。globalThis
是一个新的全局属性,通常它引用的是当前环境下的全局对象。就像是 self
对于 Web Workers,window
对于浏览器,global
对于 Node.js,以及其他实现了ES2020标准的运行环境。
1 | // Hacky globalThis polyfill you had to use pre-ES2020 |
Promise.allSettled()
这个新增的方法看起来有点像 Promise.all()
。Promise.all()
的参数中的 promise 若有一个失败,则此实例回调失败。而 Promise.allSettled()
不论成功或者失败,都会返回处理结束后的对象数组。
String.matchAll()
如果你之前使用正则,那么相比于在 while
循环中使用 RegExp.exec()
并开启标志 g
来匹配,String.matchAll()
会是更好的选择。它会返回一个包含了所有匹配结果的数组,包括捕获组的匹配结果。
1 | const regexp = /t(e)(st(\d?))/g; |