ES2019 到 ES2020 新增了哪些东西
ES2015 是 JavaScript 自上一版本之后最大的一次更新。自 ES6 之后,很多小伙伴把 JS 新增的或修正内容,都统一认为是 『ES6 新特性』。这没太大问题,不过实际上自从 2016 年之后,JS 保持了每年一个新版本的更新。最近这两年(2020年已经过去一半了),JS 继续新增和修正了部分内容。有的特性已经被应用到项目中很长时间了,有的修正则似乎对代码运行无特别影响。不过,再去熟悉一下,总是没错的。
修正 Function.prototype.toString()
Function.prototype.toString() 目前返回完整的源代码,包括注释和空格。
// 留意一下注释和方法名前的空格
function /* a comment */ foo () {}
// Previously, in V8:
foo.toString();
// → 'function foo() {}'
// ^ 没有注释
// ^ 没有空格
// Now:
foo.toString();
// → 'function /* comment */ foo () {}'
String.prototype.trimStart / String.prototype.trimEnd
之前已经有了 trim 方法,现在更加方便;为了保证兼容性,之前非标准化的 trimLeft 和 trimRight 也可继续使用。
const string = ' hello world ';
string.trimStart();
// → 'hello world '
string.trimLeft();
// → 'hello world '
string.trimEnd();
// → ' hello world'
string.trimRight();
// → ' hello world'
string.trim(); // ES5
// → 'hello world'
catch 参数可省略
之前,即使用不到 catch 里的 error,也必须加上:
try {
doSomethingThatMightThrow();
} catch (exception) {
// ^^^^^^^^^
// We must name the binding, even if we don’t use it!
handleException();
}
现在如果不需要根据异常对象做出相应错误处理,可省略:
try {
doSomethingThatMightThrow();
} catch { // → No binding!
handleException();
}
运行正常的 JSON.stringify
之前 JSON.stringify 处理一些特殊 unicode 字符时,表现怪异:
JSON.stringify('\uD800');
// → '"�"'
目前,这一情况已经得到改善;遗憾的是,JSON.parse 仍然无法返回正确结果:
JSON.stringify('\uD800');
// → '"\\ud800"'
JSON.parse(JSON.stringify('\uD800'));
// → '"�"'
Array.prototype.flat/Array.prototype.flatMap
之前拍平一个多层数组,一般通过递归或者引入 lodash 来解决:
const array = [1, [2, [3]]];
array.flat();
// → [1, 2, [3]]
// 等于
array.flat(1);
// → [1, 2, [3]]
// 递归 flat,直到数组内不再包裹数组
array.flat(Infinity);
// → [1, 2, 3]
Array.prototype.flatMap 相当于 map + flat,但效率更高:
const duplicate = (x) => [x, x];
[2, 3, 4].map(duplicate).flat(); // 🐌
// → [2, 2, 3, 3, 4, 4]
[2, 3, 4].flatMap(duplicate); // 🚀
// → [2, 2, 3, 3, 4, 4]
Object.fromEntries
Object.fromEntries 就是 Object.entries 的反响操作。Object.entries 返回对象的键值对组成的数组,而 Object.fromEntries 能根据键值对数组组合成新对象。
const object = { x: 42, y: 50 };
const entries = Object.entries(object);
// → [['x', 42], ['y', 50]]
const result = Object.fromEntries(entries);
// → { x: 42, y: 50 }
Object vs. Map
Object.entries 让对象转换为 Map 非常简单:
const object = { language: 'JavaScript', coolness: 9001 };
// Convert the object into a map:
const map = new Map(Object.entries(object));
某些情况下,我们需要把 Map 转换为对象,比如需要序列化为 JSON 的时候:
const objectCopy = Object.fromEntries(map);
// → { language: 'JavaScript', coolness: 9001 }
注意:当把 Map 转换为对象的时候,要注意 Map 的键值可以为对象,而对象的键值始终会被转换为字符串,所以以下这种情况下会出错:
const map = new Map([
[{}, 'a'],
[{}, 'b'],
]);
Object.fromEntries(map);
// → { '[object Object]': 'b' }
Symbol.prototype.description
在使用 Symbol 的时候,我们习惯于传递一个参数,起到解释说明的作用:
const symbol = Symbol('foo');
// ^^^^^
在之前,没有直接的方法能拿到 foo:
const symbol = Symbol('foo');
// ^^^^^
symbol.toString();
// → 'Symbol(foo)'
// ^^^
symbol.toString().slice(7, -1); // 🤔
// → 'foo'
以上代码能实现,但看起来就是一段魔法代码。并且,区分不了 Symbol() 和 Symbol('')。而 Symbol.prototype.description 简明扼要地解决了所有问题:
const symbol = Symbol('foo');
// ^^^^^
symbol.description; // foo
const s2 = Symbol();
s2.description; // undefined
稳定的 Array.prototype.sort 排序
我们经常用到 sort 函数来排序,但不知你注没注意到,之前地函数某些情况下返回值有点诡异:
const doggos = [
{ name: 'Abby', rating: 12 },
{ name: 'Bandit', rating: 13 },
{ name: 'Choco', rating: 14 },
{ name: 'Daisy', rating: 12 },
{ name: 'Elmo', rating: 12 },
{ name: 'Falco', rating: 13 },
{ name: 'Ghost', rating: 14 },
];
doggos.sort((a, b) => b.rating - a.rating);
虽然 Choco 和 Ghost 的 rating 值相同,不过 Choco 在原始数组中就排在前面,所以你期望的返回值应该是:
[
{ name: 'Choco', rating: 14 },
{ name: 'Ghost', rating: 14 },
{ name: 'Bandit', rating: 13 },
{ name: 'Falco', rating: 13 },
{ name: 'Abby', rating: 12 },
{ name: 'Daisy', rating: 12 },
{ name: 'Elmo', rating: 12 },
]
然而实际返回值可能是:
[
{ name: 'Ghost', rating: 14 }, // 😢
{ name: 'Choco', rating: 14 }, // 😢
{ name: 'Bandit', rating: 13 },
{ name: 'Falco', rating: 13 },
{ name: 'Abby', rating: 12 },
{ name: 'Daisy', rating: 12 },
{ name: 'Elmo', rating: 12 },
]
相当长的一段时间里,JS 并没有要求 Array#sort 的排序稳定性,JS 开发者因此也就不能依赖 sort 方法来实现排序功能。更可怕的是,某些 JS 引擎在处理较小数据量时,使用稳定排序;而在处理大数据量的数组时,使用不稳定排序。开发者在开发过程中,写了测试用例并运行通过,结果在生产环境下,数据量一大,就出现了莫名奇妙的错误。
好消息是,Array#sort 稳定排序的提案已经被接受。
JSON 成为 ES 语法的子集
你可能以为 JSON 早就是 ECMAScript 的语法子集。别惊讶,很多人都是这么认为的。
在早前的 ES 版本中,如果你的代码中出现了 U+2018 字符或者 U+2019 字符,那这些字符会被当成行终止符,代码会报出语法错误;而这些字符,是符合 JSON 语法规范的:
// A string containing a raw U+2028 character.
const LS = '
';
// → ES2018: SyntaxError
// A string containing a raw U+2029 character, produced by `eval`:
const PS = eval('"\u2029"');
在 ES2018 之后,JSON 语法和 ES 语法之间的这种差异被抹平了:
// A string containing a raw U+2028 character.
const LS = '
';
// → ES2018: SyntaxError
// → ES2019: no exception
// A string containing a raw U+2029 character, produced by `eval`:
const PS = eval('"\u2029"');
// → ES2018: SyntaxError
// → ES2019: no exception
动态 import()
Chrome 从 61 版本开始支持静态引入:
// utils.mjs
// Default export
export default () => {
console.log('Hi from the default export!');
};
// Named export `doStuff`
export const doStuff = () => {
console.log('Doing stuff…');
};
<script type="module">
import * as module from './utils.mjs';
module.default();
// → logs 'Hi from the default export!'
module.doStuff();
// → logs 'Doing stuff…'
</script>
注意:以上代码中 mjs 只是为了表明 utils.mjs 是一个模块。在 web 端,使用其他的扩展名也没有问题;不过在 Node.js 中,就必须要使用 .mjs 扩展名了。
静态导入协助优化了 tree-shaking 和打包工具,不过在面对按需引入的需求时,就束手无策了(webpack 已经提供了按需引入的方法)。
上面的代码,改写成按需引入之后的形式为:
<script type="module">
const moduleSpecifier = './utils.mjs';
import(moduleSpecifier)
.then((module) => {
module.default();
// → logs 'Hi from the default export!'
module.doStuff();
// → logs 'Doing stuff…'
});
</script>
因为 import() 返回的是一个 Promise,以上代码也可用 async/await 语法来改写:
<script type="module">
(async () => {
const moduleSpecifier = './utils.mjs';
const module = await import(moduleSpecifier)
module.default();
// → logs 'Hi from the default export!'
module.doStuff();
// → logs 'Doing stuff…'
})();
</script>
注意:虽然 import() 的使用方式看起来像是一个方法,但实际上,它是一个用到括号的特殊语法(类似于 super())。这意味着 import 并不是继承自 Function,你也就不能对它应用 apply bind 等方法;const alias = import 也是不行的;
下面的代码展示了在导航到不同页面时,如何用 import() 来实现懒加载:
<!DOCTYPE html>
<meta charset="utf-8">
<title>My library</title>
<nav>
<a href="books.html" data-entry-module="books">Books</a>
<a href="movies.html" data-entry-module="movies">Movies</a>
<a href="video-games.html" data-entry-module="video-games">Video Games</a>
</nav>
<main>This is a placeholder for the content that will be loaded on-demand.</main>
<script>
const main = document.querySelector('main');
const links = document.querySelectorAll('nav > a');
for (const link of links) {
link.addEventListener('click', async (event) => {
event.preventDefault();
try {
const module = await import(`/${link.dataset.entryModule}.mjs`);
// 模块导出了一个命名方法 `loadPageInto`.
module.loadPageInto(main);
} catch (error) {
main.textContent = error.message;
}
});
}
</script>
注意:被导入的 JS 模块不能跨域;如果跨域,需要为其添加 Access-Control-Allow-Origin: * 消息头。
BigInt
BigInt 是 JS 最新添加的一类基础类型。之前,如果我们要操作超出 Number.MIN_SAFE_INTEGER-Number.MAX_SAFE_INTEGER 范围的数值,需要借助 bn.js 一类的库,否则就会丢失精度;现在 BigInt 原生支持,一方面减小了引入包的大小,另一方面,性能更高。
Number 的问题:
const max = Number.MAX_SAFE_INTEGER;
// → 9_007_199_254_740_991
max + 1;
// → 9_007_199_254_740_992 ✅
max + 2;
// → 9_007_199_254_740_992 ❌
BigInt 基础使用:
BigInt(Number.MAX_SAFE_INTEGER) + 2n;
// → 9_007_199_254_740_993n ✅
1234567890123456789n * 123n;
// → 151851850485185185047n ✅
此外,需要注意的是,BigInt 类型的数值不能与 Number 类型的数字进行直接运算;在数值相等的情况下,BigInt 与 Number 类型数值不严格相等;Number 类型的整数可以转换为BigInt 类型后进行计算:
let big = 1n;
let n = 1;
big == n; // true
big === n; // false
big + BigInt(n); // 2n
big + n; // Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversions
模块命名空间导出
JS 模块之前已经实现了命名导入的功能:
import * as utils from './utils.mjs';
现在语法进一步升级,实现命名导出:
export * as utils from './utils.mjs';
上面代码相当于以下代码的简写:
import * as utils from './utils.mjs';
export { utils };
String.prototype.matchAll
String.prototype.match 其实已经能够满足我们的大部分需求了,不过该方法只能返回匹配的字符串,而丢失了其他信息:
const string = 'Magic hex numbers: DEADBEEF CAFE';
const regex = /\b\p{ASCII_Hex_Digit}+\b/gu;
for (const match of string.match(regex)) {
console.log(match);
}
// Output:
//
// 'DEADBEEF'
// 'CAFE'
为了获取其他信息,可以使用 exec 来循环,就是有些麻烦:
const string = 'Magic hex numbers: DEADBEEF CAFE';
const regex = /\b\p{ASCII_Hex_Digit}+\b/gu;
let match;
while (match = regex.exec(string)) {
console.log(match);
}
// Output:
//
// [ 'DEADBEEF', index: 19, input: 'Magic hex numbers: DEADBEEF CAFE' ]
// [ 'CAFE', index: 28, input: 'Magic hex numbers: DEADBEEF CAFE' ]
有了String#matchAll之后,实现相同功能就简单多了:
const string = 'Magic hex numbers: DEADBEEF CAFE';
const regex = /\b\p{ASCII_Hex_Digit}+\b/gu;
for (const match of string.matchAll(regex)) {
console.log(match);
}
// Output:
//
// [ 'DEADBEEF', index: 19, input: 'Magic hex numbers: DEADBEEF CAFE' ]
// [ 'CAFE', index: 28, input: 'Magic hex numbers: DEADBEEF CAFE' ]
String#matchAll 能返回每一个匹配的详细信息,包括分组信息:
const string = 'Favorite GitHub repos: tc39/ecma262 v8/v8.dev';
const regex = /\b(?<owner>[a-z0-9]+)\/(?<repo>[a-z0-9\.]+)\b/g;
for (const match of string.matchAll(regex)) {
console.log(`${match[0]} at ${match.index} with '${match.input}'`);
console.log(`→ owner: ${match.groups.owner}`);
console.log(`→ repo: ${match.groups.repo}`);
}
// Output:
//
// tc39/ecma262 at 23 with 'Favorite GitHub repos: tc39/ecma262 v8/v8.dev'
// → owner: tc39
// → repo: ecma262
// v8/v8.dev at 36 with 'Favorite GitHub repos: tc39/ecma262 v8/v8.dev'
// → owner: v8
// → repo: v8.dev
注意:String#matchAll 只能与有 g 标志的全局匹配正则一起使用;如果没有 g 标志,会报出语法错误。
Promise.allSettle
| 方法 | 描述 |
|---|---|
Promise.allSettled |
不会短路(已经加入到ES2020) |
Promise.all |
任意一个 rejected,则短路 |
Promise.race |
任意一个完成(非pending),则短路 |
Promise.any |
任意一个resolved,则短路(处于提案阶段) |
以下为这四个方法的一些用例:
Promise.all
const promises = [
fetch('/component-a.css'),
fetch('/component-b.css'),
fetch('/component-c.css'),
];
try {
const styleResponses = await Promise.all(promises);
enableStyles(styleResponses);
renderNewUi();
} catch (reason) {
displayError(reason);
}
只有在所有资源都加载成功的情况下,才去做展示;否则就抛出错误信息。
Promise.race
Promise.race 使用场景:
- 拿到第一个 resolved 的结果,就继续执行;
- 有一个rejected,就继续执行;
try {
const result = await Promise.race([
performHeavyComputation(),
rejectAfterTimeout(2000),
]);
renderResult(result);
} catch (error) {
renderError(error);
}
上面的代码中,如果某个耗时操作两秒钟还未完成,就抛出错误。
Promise.allSettled
Promise.allSettled 不去关注结果是 resolved 或是 rejected,它只是去表示所有动作已经执行完成。
const promises = [
fetch('/api-call-1'),
fetch('/api-call-2'),
fetch('/api-call-3'),
];
// 以下请求,有成功,有失败.
await Promise.allSettled(promises);
// 不管成功还是失败,请求完成就关掉 loading
removeLoadingIndicator();
Promise.any
Promise.any 和 Promise.race 类似,区别为只有当所有的状态都为 rejected 时,Promise.any 才会走到 catch。
const promises = [
fetch('/endpoint-a').then(() => 'a'),
fetch('/endpoint-b').then(() => 'b'),
fetch('/endpoint-c').then(() => 'c'),
];
try {
const first = await Promise.any(promises);
// 任意一个 promises 为 fulfilled.
console.log(first);
// → e.g. 'b'
} catch (error) {
// 所有 promises rejected.
console.log(error);
}
globalThis
在 ES2020 之前,为了获取不同环境下的全局对象,我们可能要借助类似于下面代码的操作:
// A naive attempt at getting the global `this`. Don’t use this!
const getGlobalThis = () => {
if (typeof globalThis !== 'undefined') return globalThis;
if (typeof self !== 'undefined') return self;
if (typeof window !== 'undefined') return window;
if (typeof global !== 'undefined') return global;
// Note: this might still return the wrong result!
if (typeof this !== 'undefined') return this;
throw new Error('Unable to locate global `this`');
};
const theGlobalThis = getGlobalThis();
注意,大多数情况下,我们都不需要去操作全局对象;globalThis 对 polyfills 或者是库开发最有用。
可选链操作符
这个功能你可能早就在项目中使用过了:?.,主要功能就是省略了之前需要写的一堆逻辑判断:
// 从 db 到 name,只要有其中一个属性值为空,则 nameLength 值为 undefined
const nameLength = db?.user?.name?.length;
搭配使用空值合并运算符,可以实现 lodash 的 get方法:
const object = { id: 123, names: { first: 'Alice', last: 'Smith' }};
{ // With lodash:
const firstName = _.get(object, 'names.first');
// → 'Alice'
const middleName = _.get(object, 'names.middle', '(no middle name)');
// → '(no middle name)'
}
{ // With optional chaining and nullish coalescing:
const firstName = object?.names?.first ?? '(no first name)';
// → 'Alice'
const middleName = object?.names?.middle ?? '(no middle name)';
// → '(no middle name)'
}
空值合并运算符
空值合并运算符 ?? 当左侧操作数为 null 或者 undefined 时,就返回右侧的值(其他false值不可):
let a = null ?? '123'; //123
a = undefined ?? 'abc'; //123
a = false ?? 'abc'; // false