ES2020和ES2021新特征学习

573 阅读14分钟

前言

Ecma International

Ecma International 是一家国际性会员制度的信息和电信标准组织,它是和企业密切相连的组织,所以Ecma国际制定的规范标准都是由各类企业来做主要的制定和推广。 ECMA-262是Ecma组织下的TC39小组制定的关于脚本语言的规范标准。

TC39

TC39 是所属于 Ecma International,由 JavaScript 开发者、实现者、学者等组成的团体,与 JavaScript 社区合作维护和发展 JavaScript 的标准TC39委员会部分成员名单

TC39 提案过程

每个 ECMAScript 的提案都将经过以下阶段,从 Stage 0 开始,从一个阶段到下一个阶段的进展必须得到 TC39 的批准:

  1. stage-0:还是一个设想,只能由TC39成员或TC39贡献者提出;
  2. stage-1:提案阶段,比较正式的提议,只能由TC39成员发起,这个提案要解决的问题必须有正式的书面描述;
  3. stage-2:草案,有了初始规范,必须对功能语法和语义进行正式描述,包括一些实验性的实现;
  4. stage-3:候选,该提议基本已经实现,需要等待实验验证,用户反馈及验收测试通过;
  5. stage-4:已完成,必须通过 Test262 验收测试,下一步就纳入ECMA标准。

ECMA-262

ECMA-262标准定义了ECMAscript语言规范。这个这个标准也叫成为ECMAscript语言规范(ECMAScript Language Specification),简称ES规范。
1997年ECMA组织发布了MCMA-262的标准,该标准制定了MCMAscript语言规范。以此语言规范为蓝本,TC39委员会制定了ECMAscript规范并在当年正式发布,这标志着ES规范的诞生。
ES规范从1997发布到现在为止已经拥有十一个版本。ECMAscript是基于几种原始技术,最著名的是javascript(netscape navigator 2.0)、jscript(microsoft ie3)、ECMA-262第六版 也就是我们常说的ES6(或者叫ES2015)。

ECMAScript和JavaScript

根据上面分析可以得到了如下结论:

  • ECMA-262是ECMA国际组织发布的一个技术标准。
  • ECMA-262中,为ECMAScript这门编程语言定义了规范。
  • JavaScript是一门遵循了 ECMAScript编程语言规范的编程语言。 那ES5,ES6,ES7...又是什么呢?
    答:它们是ECMA-262标准的版本号。 2015年ECMA国际决定每年发布一个ECMAScript版本,所以ES6被重命名为ES2015,用来体现发布的年份,因此ES6和ES2015是一个意思!以后每年的版本我们称为ES2016ES7),ES2017ES8),ES2018ES9)。

ES2020

globalThis

globalThis——通用顶层对象,是在任何环境下指向全局环境下的this。

before

  • Browser:顶层对象是window;
  • Node:顶层对象是global;
  • WebWorker:顶层对象是self。

now

直接使用通用顶层对象:globalThis。在上述三种环境中均可直接使用globalThis获得全局this
因此,globalThis 目的是提供一种标准化方式访问全局对象,有了 globalThis 后,你可以在任意上下文,任意时刻都能获取到全局对象。
如果您在浏览器上,globalThis将为window,如果您在Node上,globalThis则将为global。因此,不再需要考虑不同的环境问题。
globalThis 是一个全新的标准方法用来获取全局this

// 在全局对象中挂载一个`v`属性,赋值为`true`:
// es11之前的解决方案
const 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");
};
getGlobal().v = true;
console.log(getGlobal().v); // true

// es11用如下方式定义
globalThis.v1 = true;
console.log(globalThis.v1); // true

BigInt

BigInt大整数——任意位数的整数(新增的数据类型,使用n结尾)。
在javascript整数运算中,当计算值和结果值在Number.MIN_SAFE_INTEGERNumber.MAX_SAFE_INTEGER范围时才能保证运算结果正确,超出这个范围的整数计算或者表示会丢失精度。
因此es11加入了BigInt作为第七个原始数据类型,用于精确计算大整数运算。

一些BigInt方法

  • BigInt():转换普通数值为BigInt类型;
  • BigInt.parseInt():近似于Number.parseInt(),将一个字符串转换成指定进制的BigInt类型。

注意点;

  • BigInt同样可使用各种进制表示,都要加上后缀;
  • BigInt与Number是两种值,它们之间并不是全等的,可以进行比较,但不能进行计算;
  • typeof运算符对于BigInt类型的数据返回bigint。
let bigintA = BigInt(Number.MAX_SAFE_INTEGER);
let intB = Number.MAX_SAFE_INTEGER;
console.log(bigintA); // 9007199254740991n
console.log(bigintA == intB);// true
console.log(bigintA === intB);// false
console.log(bigintA * 2n); // 18014398509481982n

可选链操作符(?.)

可选链操作符(?.),当我们想要查找多层级的对象时,需要进行冗余的前置校验进行属性保护,否则容易报错导致程序停止运行。 而使用es11中的链判断操作符可以大大简化这种前置校验:

const user = {
  info: {
    name: "hahahh",
  },
};
// 在es11之前的写法
let name1 = user && user.info && user.info.name;
//es11之后的写法
let name2 = user?.info?.name;
console.log(name1); // hahahh
console.log(name2); // hahahh

user.info = null;
name1 = user && user.info && user.info.name;
name2 = user?.info?.name;
console.log(name1); // null
console.log(name2); // undefined

空值合并运算符(??)

空值合并操作符??)是一个逻辑操作符,当左侧的操作数为 null 或者 undefined 时,返回其右侧操作数,否则返回左侧操作数。 当我们查询某个属性时,经常会给没有该属性就设置一个默认的值,比如下面两种方式:

let a = 0,
  b = true;
let c = a ? a : b; // 方式1-三目运算符
let d = a || b; // 方式2-逻辑运算符||

console.log(c);// true
console.log(d);// true

但是上面两种方法有一个明显的弊端,它会覆盖所有的假值(0 , ' ' , false),因为左侧的操作数会被强制转换成布尔值用于求值,而这些值在某些情况下可能是有效的,使用空位合并操作符即可避免这种问题。其只有在第一个操作数为 undefinednull,才会返回其右侧表达式或变量的值。

let a = 0,
  b = true;
let c = a ? a : b; // 方式1-三目运算符
let d = a || b; // 方式2-逻辑运算符||
console.log(c); // true
console.log(d); // true

let e = a ?? b 
console.log(e); // 0

import()动态导入

import是es6中引入的全新指令,是esm规范的基础指令。
现代前端的打包资源越来越大,这些引入的资源越多,首屏加载的速度就会越慢,而前端应用初始化时并不需要全量的加载逻辑资源,,为了首屏渲染速度更快,就可以把这些模块进行按需导入。在之前,我们一般会通过webpack等打包工具对js module进行split,然后使用异步import进行加载(原理是重新发起一个get请求获取js脚本)。import()指令的出现进一步规范这种异步加载能力!

用法

关键字import可以像调用函数一样来动态的导入模块。以这种方式调用,将返回一个promise。默认地,导入的模块会在挂载在promise对象的default属性下。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>dynamic-import</title>
  </head>
  <body>
    <script>
      function handleClick() {
        import("./module.js")
          .then((module) => {
            console.log(module.default());
          })
          .catch((err) => {
            console.error("error~");
          });
      }
    </script>
    <button onclick="handleClick()">按需加载</button>
  </body>
</html>

当然,可以使用async-await语法糖来解析import()返回的promise对象:

<script>
  async function handleClick() {
    const fn = await import("./module.js");
    fn.default();
  }
</script>

特点

与import声明相比,import()有如下特点如下:

  • 能够在函数、分支等非顶层作用域使用,按需加载、懒加载都不是问题;
  • 模块标识支持变量传入,可动态计算确定模块标识;
  • 不仅限于module,在普通的script中也能使用。

注意:虽然长的像函数,但import()实际上是个操作符,因为操作符能够携带当前模块相关信息(用来解析模块表示),而函数不能。

适用场景

  1. 按需加载(比如点击时加载某个文件或者模块);
  2. 条件加载(比如if判断模块中);
  3. 动态的模块路径(比如模块路径是实时生成)。

如果希望按照一定的条件或者按需加载模块的时候,动态import() 是非常有用的。而静态型的 import 是初始化加载依赖项的最优选择,使用静态 import 更容易从代码静态分析工具和tree shaking中受益。

for-in遍历顺序

for-in操作符一般用来遍历对象的key。遍历操作必然涉及到遍历顺序,在此之前for-in的遍历顺序各浏览器厂商是有一些差异的。es11规范了这一点,保证了不同浏览器引擎的遍历结果一致!

规则

以遍历对象为例, 会先遍历出整数属性,并且以升序的顺序遍历得到对象的key,然后其他属性按照创建时候的顺序进行遍历:

const obj = {
  x2: "x2",
  x1: "x1",
  3: "3",
  1: "1",
  2: "2",
  "3xx": "3xx",
};
for (let key in obj) {
  console.log(key); // 1 2 3 x2 x1 3xx
}

补充

for-in 遍历对象要点:

  • 遍历不到 Symbol 类型的属性;
  • 遍历过程中,目标对象的属性能被删除,忽略掉尚未遍历到却已经被删掉的属性;
  • 遍历过程中,如果有新增属性,不保证新的属性能被当次遍历处理到;
  • 属性名不会重复出现(一个属性名最多出现一次);
  • 目标对象整条原型链上的属性都能遍历到。

String.prototype.matchAll

字符串处理的一个常见场景是想要匹配出字符串中的所有目标子串,然后进行输出或替换。 es11里给String对象新增了一个原型方法:matchAll,可以一次性取出所有匹配。语法:

str.matchAll(regexp)
  • 参数:regexp
    正则表达式对象。如果所传参数不是一个正则表达式对象,则会隐式地使用 new RegExp(obj) 将其转换为一个 RegExp 。
  • 返回值
    一个迭代器(不可重用,结果耗尽需要再次调用方法,获取一个新的迭代器)。
  • 注意
    RegExp必须是设置了全局模式g的形式,否则会抛出异常TypeError。

matchAll()与match()的区别

  • matchAll()不用正则也会匹配所有符合的字符串(如果使用正则,不能使用不带g修饰符的正则,否则会报错);
  • match()使用带g的正则表达式则才能可以匹配所有,否则只匹配第一个;
  • 在有多个匹配时matchAll()不像match()一样返回数组,而是返回一个迭代器,可以使用for-of遍历、数组新增的扩展符()结果或Array.from()转换为数组。
const str = "<text>JS</text><text>正则</text>";
const matchStr = str.match(/<text>(.*?)<\/text>/);
const allMatchs = str.matchAll(/<text>(.*?)<\/text>/g);
console.log(matchStr[0]);
console.log(allMatchs);
for (const match of allMatchs) {
  console.log(match[0]);
}

结果:
image.png

Promise.allSettled()

Promise.allSettled()方法返回一个在所有给定的promise都已经fulfilled或rejected后的promise,并带有一个对象数组,每个对象表示对应的promise结果。
当有多个彼此不依赖的异步任务成功完成时,或者您总是想知道每个promise的结果时,通常使用它。
相比之下,Promise.all() 更适合彼此相互依赖或者在其中任何一个reject时立即结束。
示例:(实现一个filter,过滤掉rejected状态的Promise)

async function fulfilled() {
  return Promise.allSettled([
    Promise.reject({ code: 500, msg: "服务异常" }),
    Promise.resolve({ code: 200, list: [] }),
    Promise.resolve({ code: 200, list: [] }),
  ]).then((res) => {
    console.log("all = ", res);
    // 过滤掉 rejected 状态
    return new Promise((resolve) => {
      resolve(
        res.filter((el) => {
          return el.status !== "rejected";
        })
      );
    });
  });
}

fulfilled().then((res) => {
  console.log("fulfilled = ", res);
});

结果:

image.png

ES2021

WeakRef

WeakRef对象允许保留对另一个对象的弱引用,而不会阻止被弱引用对象被GC回收。

描述

WeakRef对象包含对对象的弱引用,这个弱引用被称为该WeakRef对象的target或者是referent。对对象的弱引用是指当该对象应该被GC回收时不会阻止GC的回收行为。而与此相反的,一个普通的引用(默认是强引用)会将与之对应的对象保存在内存中。只有当该对象没有任何的强引用时,JavaScript引擎GC才会销毁该对象并且回收该对象所占的内存空间。如果上述情况发生了,那么你就无法通过任何的弱引用来获取该对象。
比如储存DOM节点,而不用担心这些节点被移除时,会引发内存泄漏。必须使用new关键字创建新的WeakRef,并把对象作为参数放入括号内,当你想读取被引用的对象时,可以在弱引用对象上调用deref()方法来实现,下面是一个很简单的例子:

let obj = {
  name: "Cache",
  size: "unlimited",
};
const myWeakRef = new WeakRef(obj);

console.log(myWeakRef.deref());
console.log(myWeakRef.deref().name);
console.log(myWeakRef.deref().size);

结果:

image.png

FinalizationRegistry

FinalizationRegistry (终结器)对象可以让你在对象被垃圾回收时请求一个回调。
FinalizationRegistry 提供了这样的一种方法:当一个在注册表中注册的对象被回收时,执行该对象拥有的终结器回调方法,即执行清理回调(清理回调有时被称为 finalizer )。

使用方法

  • 创建registry
const registry = new FinalizationRegistry(heldValue => {
  // ....
});
  • 调用register方法,注册任何你想要清理回调的对象,传入该对象和所含的值
registry.register(theObject, "some value");
  • 当需要解除对theObject对象的清理回调时:
registry.unregister(theObject);

简单示例

const a = {};
const obj = new WeakRef(a);
const fina = new FinalizationRegistry((v) => {
  console.log(v);
});

fina.register(a, "a被回收了");
console.log(a); // {}

/**** 假设此刻a被回收 ****/

// 首先打印:a被回收了
console.log(obj.deref()); // undefined

String.prototype.replaceAll()

replaceAll() 方法返回一个新字符串,新字符串所有满足 pattern 的部分都已被replacement 替换。pattern可以是一个字符串或一个 RegExp, replacement可以是一个字符串或一个在每次匹配被调用的函数。

语法

const newStr = str.replaceAll(regexp|substr, newSubstr|function)

注意:当使用一个 regex时,您必须设置全局(“ g”)标志, 否则,它将引发 TypeError:“必须使用全局 RegExp 调用 replaceAll”。

示例

let str = "a+b+c";
let s1 = str.replace(/\+/g, " ");
let s2 = str.split("+").join(" ");
let s3 = str.replaceAll("+", " ");

console.log(s1);
console.log(s2);
console.log(s3);

结果:

image.png

逻辑赋值操作符

逻辑赋值运算符结合了逻辑运算(&&,||或??)和赋值运算符的功能。
我们知道,下面的代码:

let a = 0;
a = a + 2;

可以简写为:

let a = 0;
a += 2;

ES2021中将逻辑运算符||&&??=运算符结合,得到了||=&&=??=三个逻辑赋值操作符:

let a = true,
  b = false,
  c;
a ||= b; // 等同于 a = a || b,true

a &&= b; // 等同于 a = a && b,false

c ??= b; // 等同于 c = c ?? b,false

Promise.any()

Promise.any() 接收一个Promise可迭代对象(数组、Set、Map),只要其中的一个promise成功,就返回那个已经成功的promise。如果可迭代对象中没有一个promise成功(即所有的promises 都失败/拒绝),就返回一个失败的promiseAggregateError类型的实例,它是Error的一个子类,用于把单一的错误集合在一起。这个方法可以看做是Promise.all()的反操作。 简单示例:

const promise1 = new Promise((resolve, reject) => {
  console.log("promise1 done!");
  reject("我promise1失败了");
});

const promise2 = new Promise((resolve, reject) => {
  console.log("promise2 done!");
  setTimeout(resolve, 500, "我promise2成功了");
});

const promise3 = new Promise((resolve, reject) => {
  console.log("promise3 done!");
  setTimeout(resolve, 100, "我promise3成功了");
});

Promise.any([promise1, promise2, promise3]).then((value) => {
  console.log("promise.any = ", value);
});

结果:

image.png

如果没有成功的 promise,Promise.any()返回AggregateError错误:


const promise1 = new Promise((resolve, reject) => {
  console.log("promise1 done!");
  reject("我promise1失败了");
});

const promise2 = new Promise((resolve, reject) => {
  console.log("promise2 done!");
  reject("我promise2失败了");
});

const promise3 = new Promise((resolve, reject) => {
  console.log("promise3 done!");
  reject("我promise3失败了");
});

Promise.any([promise1, promise2, promise3]).then((value) => {
  console.log(value);
});


结果:

image.png

数字分隔符

通过这个功能,我们利用 “(_U+005F】)“ 分隔符来将数字分组,提高数字的可读性:

let x = 233_333_333;
// x 的值等同于 233333333,只是这样可读性更强
console.log(x);

Intl.ListFormat

Intl.ListFormat 是一个语言相关的列表格式化构造器。语法:

new Intl.ListFormat([locales[, options]])

ListFormat 对象的构造方法有两个参数,皆为可选。

locales:是一个语言标识,符合 BCP 47 语言标注的字符串或字符串数组,可选值有en、zh-Hans-CN、it、de等等;
options:第二个参数是一个选项对象 -- 包含了localeMatcher、style 和 type 三个属性。

示例:

const arr = ["Pen", "Pencil", "Paper"];

let obj = new Intl.ListFormat("en", { style: "short", type: "conjunction" });
console.log(obj.format(arr)); // Pen, Pencil, & Paper

obj = new Intl.ListFormat("zh-Hans-CN", {
  style: "short",
  type: "conjunction",
});
console.log(obj.format(arr)); // Pen, Pencil, Paper

obj = new Intl.ListFormat("en", { style: "long", type: "conjunction" });
console.log(obj.format(arr)); // Pen, Pencil, and Paper

obj = new Intl.ListFormat("en", { style: "narrow", type: "conjunction" });
console.log(obj.format(arr)); // Pen, Pencil, Paper

// 传入意大利语标识
obj = new Intl.ListFormat("it", { style: "short", type: "conjunction" });
console.log(obj.format(arr)); // Pen, Pencil e Paper

// 传入德语标识
obj = new Intl.ListFormat("de", { style: "long", type: "conjunction" });
console.log(obj.format(arr)); // Pen, Pencil e Paper

结果:

image.png