ECMAScript 2020(ES11)新特性

ECMAScript 2020(ES11)新特性:

  • ?? 操作符
  • ?. 操作符
  • 导出加强: export
  • 动态导入: await import()
  • 元信息: import.meta
  • 等待所有的 Promise 结束: Promise.allSettled()
  • 正则模式: matchAll()
  • 任意精度的整数: BigInt
  • 标准化对象: globalThis


?? 操作符

?? 操作符是一个判断是否为空然后赋值的操作,如果没有这个操作符,我们通常使用 || 来简单的进行这个操作,如下所示:

const yourAge = someBody.age || 18

上面的代码意思是如果 someBody.age 是空,那么就将 yourAge 设置成为 18。但是上面代码有个问题,如果 someBody.age=0 的话,上述逻辑也成立。使用 ?? 操作符可以解决这个问题。

const yourAge = someBody.age ?? 18


?. 操作符

我们有时候在获取某个对象的属性的时候,需要进行对象的 null 判断,否则从 null 对象中取出属性就会报错,但是通常的 ?: 操作符使用起来太复杂了,如果有多个对象和属性连写的情况下更是如此,如果使用 ?. 操作符就会简单很多:

const age = school?.class?.student?.age;

同样 ?. 还可以用在对象的方法上:

const age = student.getAge?.();

上面代码表示,如果 student getAge() 方法存在,则调用,否则返回 undefined


动态导入

在 ES11 之前,我们可以使用下面的方式进行模块的导入:

import * as TestModule from "./test-module.js";

但是上面的导入方式会有一些问题,首先是效率的问题,所有的 module 都需要在首次加载的时候导入,会导致程序效率的降低。另外上面的模块名字是写死的,不可以在程序运行的时候进行动态修改。也就是说上面的模块导入方式,不能对模块进行动态导入,或者按需导入,在使用上有诸多的不便。

为了解决这个问题,ES11 引入了新的 import() 方法,使用这个方法,你可以对模块进行动态导入,并且通过设置模块名为变量的形式,可以对模块名进行动态修改。动态导入返回请求模块的模块名称空间对象的 promise 。因此,可以配合使用 async/await。

const baseModulePath = "./baseModules";
const btnImportModule = document.getElementById("btnImportModule");
let userList = [];
btnImportModule.addEventListener("click", async e => {
  const userModule = await import(`${baseModulePath}/users.js`);
  userList = userModule.getUsers();
});
element.addEventListener('click', async() => {
    const module = await import(`./api-scripts/button-click.js`);
    module.clickEvent();
})


import.meta

除了动态引入模块之外,import 还提供了一个元属性 meta ,它包含了当前引入的模块的信息,目前他里面有一个 url 属性,代表模块被引用的 URL。如果想使用 URL 信息,那么可以在代码中使用 import.meta.url

import.meta ,返回当前模块的元信息。 import.meta 只能在模块内部使用,如果在模块外部使用会报错。这个属性返回一个对象,该对象的各种属性就是当前运行的脚本的元信息。具体包含哪些属性,标准没有规定,由各个运行环境自行决定。一般来说, import.meta 至少会有下面两个属性。

import.meta.url

import.meta.url 返回当前模块的 URL 路径。举例来说,当前模块主文件的路径是 https://foo.com/main.js import.meta.url 就返回这个路径。如果模块里面还有一个数据文件 data.txt ,那么就可以用下面的代码,获取这个数据文件的路径。

new URL('data.txt', import.meta.url)

注意,Node.js 环境中, import.meta.url 返回的总是本地路径,即是 file:URL 协议的字符串,比如 file:///home/user/foo.js


import.meta.scriptElement

import.meta.scriptElement 是浏览器特有的元属性,返回加载模块的那个 <script> >元素,相当于 document.currentScript 属性。

// HTML
// <script type="module" src="my-module.js" data-foo="abc"></script>
// my-module.js 内部执行下面的代码
import.meta.scriptElement.dataset.foo
// "abc"


export 加强

对于导出模块需要重命名的情况,我们不能直接导出,而是必须先在 import 的时候进行重命名,然后再使用 export 将重命名的模块导出:

import * as myModule from "./test-module.js";
export {myModule};

如果使用 export 的增强,则不需要使用 import ,直接使用 export 导出即可:

export * as {myModule} from "./test-module.js";


Promise.allSettled()

自从 Promise 引入之后,有两个方法可以对 Promise 进行组合,分别是 Promise.all() Promise.race() ,他们分别表示返回所有的 Promise 和返回最快的那个。

对于 Promise.all() 来说,它会等待所有的 Promise 都运行完毕之后返回,如果其中有一个 Promise 被 rejected,那么整个 Promise.all() 都会被 rejected。在这种情况下,如果有一个 Promise 被 rejected,其他的 Promise 的结果也都获取不了。

为了解决这个问题,ES11 引入了 Promise.allSettled() 方法,这个方法会等待所有的 Promise 结束,不管他们是否被 rejected。

const promises = [promise1("/do1"), promise2("/do2")];
const allResults = await Promise.allSettled(promises);
const errors = results
  .filter(p => p.status === 'rejected')
  .map(p => p.reason);


String.protype.matchAll()

以前,带字符串参数的 String.match() 仅返回第一个匹配。

let string = 'Between';
let matches = string.match('e');
console.log(matches[0]); // "e"

添加 /g 模式:

let string = 'Between';
let ret = string.match(/e/g);  // (3) ["e","e","e"];

ES11 引入了 matchAll() 方法。这个方法可以简单的返回一个遍历器,通过遍历这个遍历器,就可以得到所有的匹配的值,如下所示:

const regExp = /yyds(\d+)/g;
const text = 'yyds1 is a very good yyds2';
let matches = [...text.matchAll(regExp)];
for (const match of matches) {
  console.log(match);
}


使用 .matchAll() 的好理由:

  • 在与捕获组一起使用时,它可以更加优雅,捕获组只是使用 () 提取模式的正则表达式的一部分。
  • 它返回一个迭代器而不是一个数组,迭代器本身是有用的。
  • 迭代器可以使用扩展运算符( )转换为数组。
  • 它避免了带有 /g 标志的正则表达式,当从数据库或外部源检索未知正则表达式并与陈旧的 RegEx 对象一起使用时,它非常有用。
  • 使用 RegEx 对象创建的正则表达式不能使用点( . )操作符链接。
  • 高级: RegEx 对象更改跟踪最后匹配位置的内部 .lastindex 属性,这在复杂的情况下会造成严重破坏。
const string = 'black*raven lime*parrot white*seagull';
const regex = /(?<color>.*?)\*(?<bird>[a-z0-9]+)/;
for (const match of string.matchAll(regex)) {
  let value = match[0];
  let index = match.index;
  let input = match.input;
  console.log(`${value} at ${index} with '${input}'`);
  console.log(match.groups.color);
  console.log(match.groups.bird);
}


BigInt

ES11 引入了新的数据类型 BigInt,在这之前,javascript 中表示数字的对象是 Number ,它可以表示 64-bit 的浮点类型数字。当然它也可以代表整数,但是整数表示的最大值是 2^53 ,也可以用 Number.MAX_SAFE_INTEGER 来表示。

一般来说Number已经够用了,但是如果在某些情况下需要对 64-bit 的整数进行存储或者运算,或者要表示的范围超过了 64-bit 的话,Number 就不够用了。怎么办呢?如果只是存储的话,可以存储为字符串,但是第二种字符串就不适用了。于是引入了BigInt来解决这个问题。要表示 BigInt,只需要在数字的后面加上 n 即可。

BigInt 是第七种原始类型。BigInt 是一个任意精度的整数。这意味着变量现在可以表示 ²⁵³ 数字,而不仅仅是 9007199254740992。

const bigInt = 112233445566778899n;

或者使用构造函数来构造 bigInt:

const bigInt = BigInt("112233445566778899");

可以使用 typeof 来查看 bigInt 的类型。要注意的是虽然Number和BigInt都代表的是数字,但是两者是不能混用的,你不能将一个 Number 和一个 BigInt 相加。这会报 TypeError 异常。如果非要进行操作,那么可以使用 BigInt 构造函数将 Number 转换成为 BigInt 之后再进行。


const b = 1n; // 追加 n 以创建 BigInt

在过去,不支持大于 9007199254740992 的整数值。如果超过,该值将锁定为 MAX_SAFE_INTEGER + 1:

const limit = Number.MAX_SAFE_INTEGER;
// 9007199254740991

limit + 1;
// 9007199254740992

limit + 2;
// 9007199254740992 <--- MAX_SAFE_INTEGER + 1 exceeded

const larger = 9007199254740991n;
// 9007199254740991n

const integer = BigInt(9007199254740991); // initialize with number
// 9007199254740991n

const same = BigInt("9007199254740991"); // initialize with "string"
// 9007199254740991n


globalThis 对象

在 ES10 之前, globalThis 还没有标准化,在平时的项目可以通过以下方式来兼容不同的平台。

var getGlobal = function () {
  if (typeof self !== 'undefined') { return self; }
  if (typeof window !== 'undefined') { return window; }
  if (typeof global !== 'undefined') { return global; }
  throw new Error('unable to locate global object');
};

但即使这样也不总是奏效。因此,ES10 添加了 globalThis 对象,从现在开始,该对象用于在任何平台上访问全局作用域。