ES2020,你需要知道的一切内容!

9,803 阅读8分钟

译原文:www.martinmck.com/posts/es202…

作者:Martin McKeaveney

译:黄梵高

如何给JavaScript增加新的特性?

并非是Google,或者是其他权力巅峰的人。JavaScript规范由称为TC39的委员会进行管理和迭代。TC39由各种开发人员,学术界人士和平台爱好者组成。

TC39每年约召开6次会议,大部分在美国,但在欧洲也举行。他们与社区合作,接受有关JavaScript新功能的建议,并逐步处理JavaScript语言建议的四个“阶段”。这四个阶段如下:

Stage 0: strawman 一种推进ECMAScript发展的自由形式,任何TC39成员,或者注册为TC39贡献者的会员,都可以提交。通常通过针对TC39 ECMAScript GitHub存储库提高PR来完成此操作

Stage 1: proposal 该阶段产生一个正式的提案。

  • 确定一个带头人来负责该提案,带头人或者联合带头人必须是TC39的成员。
  • 描述清楚要解决的问题,解决方案中必须包含例子,API以及关于相关的语义和算法。
  • 潜在问题也应该指出来,例如与其他特性的关系,实现它所面临的挑战。
  • polyfill和demo也是必要的。

围绕提案创建了一个公共GitHub存储库,其中包含示例,高级API,基本原理和潜在问题。

Stage 2: draft 草案是规范的第一个版本,与最终标准中包含的特性不会有太大差别。 草案之后,原则上只接受增量修改。

  • 草案中包含新增特性语法和语义的,尽可能的完善的形式说明,允许包含一些待办事项或者占位符。
  • 必须包含2个实验性的具体实现,其中一个可以是用转译器实现的,例如Babel。

“草案”阶段意味着需要确定提案的所有语法和语义。这涉及使用您将在JavaScript规范本身中看到的正式规范语言描述提案功能。

Stage 3: candidate 候选阶段,获得具体实现和用户的反馈。 此后,只有在实现和使用过程中出现了重大问题才会修改。

  • 规范文档必须是完整的,评审人和ECMAScript的编辑要在规范上签字。
  • 至少要有两个符合规范的具体实现

在此阶段,责任在于社区。开发人员应使用该功能并提供反馈,这只有通过在实际开发中使用它才能实现。

Stage 4: finished 已经准备就绪,该特性会出现在年度发布的规范之中。

  • 通过Test 262验收测试
  • 有2个通过测试的实现,以获取使用过程中的重要实践经验。
  • ECMAScript的编辑必须规范上的签字。

尘埃落定。该建议已在社区中的实际实施中得到了很好的测试。本提案将包含在下一版ECMAScript标准中,并将被数以百万计的人使用。

下面就是es2020新特性的介绍!

String.prototype.matchAll

String.prototype.matchAll是一个实用函数,用于获取特定正则表达式的所有匹配项(包括捕获组,这将在后面说明)。在ES2020之前如何解决此问题?让我们来看一个简单的示例并进行迭代:

const test = "climbing, oranges, jumping, flying, carrot";

目标是获取所有以ing结尾的单词,并且返回他们去掉ing的动词格式

  • 在字符串中搜索以“ ing”结尾的任何单词(例如“ climbing”)
  • 捕获单词中“ ing”之前的所有字母(例如“ climb”)
  • 返回字符串

为了达到目的,我们使用下面的正则:

const regex = /([a-z]*)ing/g;

正则表达式很难,让我们对其分解,了解每一部分的原理。

  • ([a-z]*)-匹配任何连续包含字母a到z的字符串。我们将其包装在括号中(),以使其成为“捕获组”。顾名思义,捕获组就是“捕获”与该特定部分匹配的字符组。在我们的示例中,我们希望匹配所有以“ ing”结尾的单词,但是我们真正想要的是之前的字母,因此使用捕获组。
  • ing -仅匹配以“ ing”结尾的字符串。
  • /g-全局搜索。搜索整个输入字符串。不要在第一次匹配成功后就停下来。

String.prototype.match

我们希望通过正则表达式查找动词。JavaScript中的match函数可以传入正则表达式。

const test = "climbing, oranges, jumping, flying, carrot";
const regex = /([a-z]*)ing/g;

test.match(regex);

// ["climbing", "jumping", "flying"]

但这并不是我们想要的。它返回了完整的单词,而不只是动词!发生这种情况是因为match不支持使用/g标志捕获组。match在不需要使用捕获组的时候很好用,但现在似乎不太适合。

来自@大圣巴巴 的提示

"climbing, oranges, jumping, flying, carrot".match(/(\w+)(?=ing)/g)

也可以输出想要的动词结果

当然如果想获得匹配的索引位置等信息,最方便的方式还是使用String.prototype.matchAll()

RegExp.prototype.exec

exec方法在正则表达式本身上执行,而不是在类似字符串上执行matchexec支持捕获组,但是使用的API稍显笨拙。您必须不断exec在正则表达式上反复调用以获取下一个匹配项。这要求我们创建一个无限循环并持续调用,exec执行到没有匹配项为止。

const regex = /([a-z]*)ing/g;

const matches = [];

while (true) {
  const match = regex.exec(test);
  if (match === null) break;
  matches.push(match[1]);
}

matches
// ["climb", "jump", "fly"]

这种方法是可以满足需求的,但是有点混乱不清晰。这样做的主要原因有两个:

  • 仅当/g标志设置在末尾时,它才执行预期的操作。如果您将正则表达式作为变量或参数传递,这可能会引起混淆。
  • 使用/g标志时,RegExp对象是有状态的,并存储对其最后匹配项的引用。而这一点有可能导致bug,如果你重复且不同的调用它。

使用String.prototype.matchAll

终于,我们到了。String.prototype.matchAll这将使我们的生活更加轻松,并提供一个简单的解决方案来支持捕获组,并返回一个可迭代的数组,该数组可以扩展为数组。让我们将上面的代码重构使用matchAll。

const test = "climbing, oranges, jumping, flying, carrot";

const regex = /([a-z]*)ing/g;

const matches = [...test.matchAll(regex)];

const result = matches.map(match => match[1]);

result

// ["climb", "jump", "fly"]

我们得到一个二维数组,第一个元素中的单词全匹配('climbing'),第二个元素中的捕获组('climb')。通过迭代并保留第二个元素,终于我们得到想要的结果。

动态import()

由于webpack的支持,这是您可能已经熟悉的一种方式。并且在生产JavaScript应用程序中经常使用它来进行“代码拆分”。代码拆分在单个页面应用程序中非常强大。在许多情况下,可以大大加快初始页面加载时间。

动态导入语法允许我们将import作为能够返回promise的函数进行调用。这对于在代码运行时动态加载模块特别有用。例如,您可能想基于代码中的某些逻辑来加载某个组件或模块。

// JavaScript for side panel is loaded
  const sidePanel = await import("components/SidePanel");
  sidePanel.open()

还支持插值。

async function openSidePanel(type = "desktop") {
    // JavaScript for desktop side panel is loaded
    const sidePanel = await import(`components/${type}/SidePanel`);
    sidePanel.open();
}

这个新功能增强了我们应用程序的性能。我们不必预先加载所有的JavaScript。动态导入使我们能够仅加载所需数量的JS控件,性能上极大提升。

BigInt

JavaScript可以处理的最大数量为2^53。就是这样9007199254740991,或者您可以使用更好记一点的Number.MAX_SAFE_INTEGER

当你数字超过MAX_SAFE_INTEGER时会发生什么?

console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(Number.MAX_SAFE_INTEGER + 1); // 9007199254740992
console.log(Number.MAX_SAFE_INTEGER + 2); // 9007199254740992 - wut
console.log(Number.MAX_SAFE_INTEGER + 3); // 9007199254740994 - WUT

BigInt是ES2020中新增的类型,用于解决此问题。要将number或者string转换为BigInt,可以使用BigInt构造函数,也可以n在其末尾添加a 。因此,要修正上面的示例,在将2加到后得到相同的值Number.MAX_SAFE_INTEGER

BigInt(Number.MAX_SAFE_INTEGER) + 2n; // 9007199254740993n ✅

谁需要这些数字呢?

您可能会惊讶于在软件开发中拥有如此庞大的数字非常普遍。比如时间戳和唯一标识符就可以是这么大的数字。

例如,Twitter使用如此大的整数作为推文的唯一键。如果您尝试将JavaScript存储为不带数字的数字,则会在JavaScript应用程序中看到奇怪的错误BigInt。您将不得不使用第三方社区包,或者将它们存储为字符串。这也是JavaScript开发人员在BigInt不支持的环境中解决此问题的常用解决方案。

Promise.allSettled

假设您正在参加考试。收到结果后,您发现您正确回答了99%的问题。在大多数生活领域中,您都会充满对胜利的希望。不过,在这种情况下,您依然会在得分中收到一个大红色的叉,告诉您您失败了。

这就是Promise.all的工作方式。Promise.all接受一系列承诺,并同时获取其结果。如果它们全部成功,您的Promise.all成功。如果一项或多项失败,您的Promise就会被reject。在某些情况下,您可能需要这种处理方式,但事实上并不总是这样。

引入Promise.allSettled

ES2020的Promise.allSettled在考试方面可要比Promise.all好得多。它将使您轻拍一下,并告诉您不要担心1%的reject

Promise出现时,无论它被认为是resolverejectPromise.allSettled允许我们传递一系列的Promise,这些Promise将在全部结束后,Promise的返回值是一个装满Promise结果的数组。让我们看一个例子:

const promises = [
  fetch('/api1'),
  fetch('/api2'),
  fetch('/api3'),
];

Promise.allSettled(promises).
  then((results) => results.forEach((result) => console.log(result.status)));

// "fulfilled"
// "fulfilled"
// "rejected"

globalThis

globalThis 是一个全新的标准方法用来获取全局 this 。之前开发者会通过如下的一些方法获取:

  • 全局变量 window:是一个经典的获取全局对象的方法。但是它在 Node.js 和 Web Workers 中并不能使用
  • 全局变量 self:通常只在 Web Workers 和浏览器中生效。但是它不支持 Node.js。一些人会通过判断 self - 是否存在识别代码是否运行在 Web Workers 和浏览器中
  • 全局变量 global:只在 Node.js 中生效

由于Js的通用性,相同的JavaScript代码可以在NodeJS的客户端和服务器上运行。这提出了一系列特殊的挑战。

一个是全局对象,可以从任何运行的代码段中访问它。window在浏览器中,但globalNode中。编写访问此全局对象的通用代码依赖于一些条件逻辑,这些条件逻辑可能看起来像这样(蒙住眼睛)。

(typeof window !== "undefined"
? window
: (typeof process === 'object' &&
   typeof require === 'function' &&
   typeof global === 'object')
    ? global
    : this);

值得庆幸的是,ES2020带来了globalThis全局变量。现在可以放松的不去考虑windowglobal而统一前端或后端代码。

globalThis.something = "Hello"; // Works in Browser and Node.

for-in的学问

for (x in obj) ... 是在很多时候都超级有用的语法,主要是遍历对象的key值。

for (let key in obj) {
  console.log(key);                      
}

该提议与在循环中迭代元素的顺序和语义有关。在提出这个之前,大多数JavaScript引擎已经应用了常识,所有主流浏览器都按照定义它们的顺序遍历对象的属性。但是,有些细微差别。这些主要涉及更高级的功能,例如代理。for..in循环语义从一开始就没有包含在JavaScript规范中,但是该提议可确保每个人在for..in工作方式上都具有一致的参考点。

可选链操作符(Optional Chaining)

可选链接可能是相当长一段时间以来JavaScript中最受期待的功能之一。在对更干净的JavaScript代码的影响方面,这一得分非常高!!!

当检查嵌套对象内部的属性时,通常必须检查中间对象的存在。让我们来看一个例子:

const test = {
  name: "foo",
  age: 25,
  address: {
    number: 44,
    street: "Sesame Street",
    city: {
      name: "Fake City",
      lat: 40,
      lon: 74
    }
  }
}

// when we want to check for the name of the city
if (test.address.city.name) {
  console.log("City name exists!");
}

// City Name exists!

这是一个成功的例子!但是在开发中,不能总是依靠幸福的曙光照耀我们。有时中间的值会不存在。让我们看同样的例子,但是没有给city定义值。

const test = {
  name: "foo",
  age: 25,
  address: {
    number: 44,
    street: "Sesame Street"
  }
}

if (test.address.city.name) {
  console.log("City name exists!");
}

// TypeError: Cannot read property 'name' of undefined

我们的代码已经报错了。这是因为我们试图访问nametest.address.city,但是这是一个undefined。当尝试读取undefined上的属性时TypeError肯定会抛出以上内容。那我们该如何解决?在之前许多JavaScript代码中,您将看到以下解决方案:

const test = {
  name: "foo",
  age: 25,
  address: {
    number: 44,
    street: "Sesame Street"
  },
  
}

if (test.address && test.address.city && test.address.city.name) {
  console.log("City name exists!");
}

// no TypeError thrown!

我们的代码现在可以运行了,但是我们不得不在出bug那儿写很多代码来解决问题。我们按理来说可以做得更好!ES2020的可选链接运算符让我们可以新加一个?(可选链操作符语法?.),检查对象深处是否存在值。这是使用可选链接运算符重写的上述示例:

const test = {
  name: "foo",
  age: 25,
  address: {
    number: 44,
    street: "Sesame Street"
  },
  
}

// much cleaner.
if (test?.address?.city?.name) {
  console.log("City name exists!");
}

// no TypeError thrown!

看起来不错。我们将十分长的&&链,浓缩为更加简洁易读的可选链运算符。如果链中的任何值是null或者 undefined,则表达式仅返回undefined

可选的链接运算符非常强大。请看以下示例可以使用它的其他方式:

const nestedProp = obj?.['prop' + 'Name']; // computed properties

const result = obj.customMethod?.(); // functions

const arrayItem = arr?.[42]; // arrays

空位合并运算符(Nullish coalescing Operator)

空位合并运算符是一个非常简单的名称,听起来很花哨。此功能使我们能够检查一个值是否为nullundefined,如果是,则默认为另一个值,仅此而已。

为什么这有用?让我们假设一下,你的JavaScript中有五个虚值。

  • null
  • undefined
  • 空字符串 ""
  • 0
  • 没有数字-NaN

我们可能有一些要检查数值的代码。我们想为球队中的球员分配一个小队号码。如果他们已经有小队号码,我们将保留该号码。否则,我给他们一个叫"unssigned"的值。

const person = {
  name: "John",
  age: 20,
  squadNumber: 100
};

const squadNumber = person.squadNumber || "unassigned";

console.log(`${person.name}s squad number is ${squadNumber}`);


// "Johns squad number is 100"

此代码可以正常工作。但是,让我们从一个稍微不同的角度来考虑。如果我们的person.squadNumber0,该怎么办?

const person = {
  name: "Dave",
  age: 30,
  squadNumber: 0
};

const squadNumber = person.squadNumber || "unassigned";

console.log(`${person.name}s squad number is ${squadNumber}`);


// "Daves squad number is unassigned"

这是不对的。Dave已经为球队效力了多年。我们的代码有一个错误。发生这种情况是因为0,导致我们false条件的||被调用。在此示例中,对结果值的检查存在问题。您当然可以通过以下操作解决此问题:

const person = {
  name: "Dave",
  age: 30,
  squadNumber: 0
};

const squadNumber = person.squadNumber >= 0 ? person.squadNumber : "unassigned";

console.log(`${person.name}s squad number is ${squadNumber}`);


// "Daves squad number is 0"

这是一个还可以的解决方案,但我们可以使用空位合并运算符来解决。

运算符??可以更好地确保我们的值是nullundefined

const person = {
  name: "Dave",
  age: 30,
  squadNumber: 0
};

// Nullish Coalescing Operator
// If person.squadNumber is null or undefined
// set squadNumber to unassigned
const squadNumber = person.squadNumber ?? "unassigned";

console.log(`${person.name}s squad number is ${squadNumber}`);


// "Daves squad number is 0"

import.meta

import.meta是一个给JavaScript模块暴露特定上下文的元数据属性的对象。它包含了这个模块的信息,比如说这个模块的URL。

如果您熟悉Node,则可以通过__dirname__filename属性与CommonJS一起使用此功能。

const fs = require("fs");
const path = require("path");
// resolves data.bin relative to the directory of this module
const bytes = fs.readFileSync(path.resolve(__dirname, "data.bin"));

那浏览器呢?这里import.meta将会变得有用。如果要从浏览器中运行的JavaScript模块导入相对路径,则可以让import.meta这样做:

// Will import cool-image relative to where this module is running.
const response = await fetch(new URL("../cool-image.jpg", import.meta.url));

此功能对开发第三方库的作者那可是非常有用了,因为他们不知道他们的代码将在什么地方以什么方式运行的。

结论

总而言之,ECMAScript规范中添加的最新功能为不断发展和发展的JavaScript生态系统增加了更多实用性,灵活性和强大功能。看到社区继续以如此快的速度蓬勃发展和进步,真的令人鼓舞和激动。

您可能在想:“这些听起来不错,但是我如何在我的项目里使用ES2020功能呢?

什么时候,如何使用这些东西?

现在就可以使用它!在大多数现代浏览器和Node的最新版本中,支持所有的功能。caniuse.com是另一个很棒的网站,可用于检查跨浏览器和Node的ES2020新增功能的兼容性级别。

如果需要在较旧的浏览器或Node版本上使用这些功能,则需要babel / typescript

希望你学到了一些东西。谢谢阅读!

参考文献:

  1. github.com/tc39/propos…
  2. prop-tc39.now.sh/
  3. www.martinmck.com/posts/es202…