JavaScript 的应用领域已经从 Web 浏览器扩展到所有需要编程的地方。

  1. Node.js — 用于CLI和服务器。
  2. Electron — 用于跨平台的桌面应用程序。
  3. React native — 用于跨平台的移动应用。
  4. IoT — 低成本物联网设备现在开始支持 javascript。

最近更新的 V8 引擎使性能提升了不少。JavaScript 解析速度提高了 2 倍甚至更快,从node v8.0开始,node v11以上版本的平均速度比 node v8.0 提高了 11 倍。内存消耗减少了 20%。 在性能和可用性上有了全面改善。

在本文中,我们将看到一些可以在Chrome浏览器(版本 ≥ 76)或 Node.js(版本 ≥ 11)CLI 中测试的 ES10 强大功能。

Private Class Field, String.matchAll(), Numeric Separators BigInt, Local String BigInt, GlobalThis, Promise.allSettled(), Dynamic Import

私有类字段

在ES6之前,我们无法直接申请 private 属性。 是的,有下划线约定(_propertyName)、闭包、 symbols 或 WeakMaps 等方法。

但现在私有类字段可以使用哈希前缀 # 来定义。让我们通过实例学习它

class Test {
  a = 1;          // .a is public
  #b = 2;         // .#b is private
  static #c = 3;  // .#c is private and static
  incB() {
    this.#b++;
  }
}
const testInstance = new Test();
// runs OK
testInstance.incB();
// error - private property cannot be modified outside class
testInstance.#b = 0;

注意:截至目前,没有办法定义私有函数,尽管 TC39 第 3 阶段:建议草案 建议在名字上使用散列前缀 🤞

String.matchAll()👇

如果我有一个字符串,其中有多个全局正则表达式捕获组,我经常想要遍历所有匹配。目前,我的选择有以下几种:

  1. RegExp.prototype.exec() with /g — 我们可以称之为 .exec() 多次获得一个正则表达式的匹配。它为每个匹配返回一个匹配对象,最后返回 null。
  2. String.prototype.match() with /g — 如果我们通过 .match() 使用正则表达式,设置其标志为 /g ,你会得到一个完全匹配的数组。
  3. String.prototype.split() — 如果我们使用分割字符串和正则表达式来指定分隔符,并且它至少包含一个捕获组,那么 .split() 将返回一个子串交错的数组。

上述方法的问题在于,只有在正则表达式上设置 /g 并且每次匹配时对正则表达式的属性 .lastIndex 进行更改时,它们才起作用。 这使得在多个位置使用相同的正则表达式存在风险。

matchAll() 能够帮助解决以上所有问题。让我们看看它的定义和使用:

给定字符串和正则表达式,.matchAll() 返回与正则表达式匹配的所有结果,包括捕获组。

let regexp = /t(e)(st(\d?))/g;
let str = 'test1test2';let array = [...str.matchAll(regexp)];console.log(array[0]);
// expected output: Array ["test1", "e", "st1", "1"]

注意:.matchAll() 返回一个迭代器,但它不是真正的可重启迭代器。 也就是说一旦结果耗尽,则需要再次调用该方法并创建一个新的迭代器。

数字分隔👇

如果你一直在努力去读较长的数字序列,那么这就是你要找的。

数字分隔符使人眼能够快速解析,尤其是当有很多重复的数字时:

1000000000000 -> 1_000_000_000_000
1019436871.42 -> 1_019_436_871.42

现在,更容易说出第一个数字是1万亿,而第二个数字是10亿。

这也适用于其他进制,例如:

const fileSystemPermission = 0b111_111_000;
const bytes = 0b1111_10101011_11110000_00001101;
const words = 0xFAB_F00D;

你还可以用在分数和指数中:

const massOfElectronInKg = 9.109_383_56e-31;
const trillionInShortScale = 1e1_2;

注意:解析带有 _ 分隔的整数可能很棘手,因为Number('123_456') 会给出 NAN,而 parseInt('123_456') 则给出 *123*。

BigInt👇

BigInts 是 JavaScript 中的一种新的数字原语,可以表示精度比2⁵³-1更大的整数。 使用 BigInts,你可以安全地存储和操作大整数,甚至可以超出 Numbers 的安全整数限制。

BigInts 可以正确执行整数运算而不会溢出。 让我们通过一个例子来理解:

const max = Number.MAX_SAFE_INTEGER;
// 9007199254740991
max+1;
// 9007199254740992
max+2;
// 9007199254740992

我们可以看到 max + 1 产生的结果与 max + 2 相同。

任何超出安全整数范围(即从 Number.MIN_SAFE_INTEGERNumber.MAX_SAFE_INTEGER)的整数的计算都可能会失去精度。所以我们只能依赖安全范围内的数字整型的值。

BigInts 应运而生,可以通过将 n 后缀添加到整数文字中来创建 BigInts 。例如,123 变成 123n,或者全局 BigInt(number) 函数可用于将 Number 转换为 BigInts

让我们重新看一下上面的 BigInt 例子

BigInt(Number.MAX_SAFE_INTEGER) + 2n;
// 9007199254740993ntypeof 123n
// "bigint2"

注意:数字分隔符对于BigInts尤其有用,例如:

const massOfEarthInKg = 6_000_000_000_000_000_000_000_000n;

BigInts 支持最常见的运算符。二进制 +-*** 均按预期工作。 / 工作时根据需要四舍五入。

(7 + 6 - 5) * 4 ** 3 / 2 % 3;
// → 1
(7n + 6n - 5n) * 4n ** 3n / 2n % 3n;
// → 1n

注意:它不允许在 BigIntsNumbers 之间进行混合运算。

BigInt 的语言环境字符串👇

toLocaleString() 方法返回一个字符串,该字符串具有 BigInt 的语言敏感表示形式。

let bigint = 123456789123456789n;

// 德国使用 thousands
console.log(bigint.toLocaleString('de-DE'));
// → 123.456.789.123.456.789

//在大多数说阿拉伯语的国家中,阿拉伯语使用东部阿拉伯数字
console.log(bigint.toLocaleString('ar-EG'));
// → ١٢٣٬٤٥٦٬٧٨٩٬١٢٣٬٤٥٦٬٧٨٩

// 印度使用 thousands/lakh/crore 分隔符
console.log(bigint.toLocaleString('en-IN'));
// → 1,23,45,67,89,12,34,56,789

// nu 扩展用于请求编号系统,例如 中文数字
console.log(bigint.toLocaleString('zh-Hans-CN-u-nu-hanidec'));
// → 一二三,四五六,七八九,一二三,四五六,七八九

// 请求不支持的语言(例如巴厘语)时,请使用后备语言(在这种情况下为印尼语)
console.log(bigint.toLocaleString(['ban', 'id']));
// → 123.456.789.123.456.789

globalThis 关键字👇

JavaScript 的变量作用域被嵌套并形成树结构,其根是全局作用域,this 关键字的值是对 “拥有” 当前正在执行的代码或所查看函数的对象的引用。

要了解有关此关键字和全局作用一的更多信息,请阅读以下文章

Scopes in Javascript

Understanding Javascript ‘this’ keyword (Context)

通常要弄清楚全局作用域,我们使用这样的函数

const getGlobalThis = () => {
  
  // 在 webworker 或 service worker 中
  if (typeof self !== 'undefined') return self;
  
  // 在浏览器中
  if (typeof window !== 'undefined') return window;
  
  // 在 Node.js 中
  if (typeof global !== 'undefined') return global;
  
  // 独立的 JavaScript shell
  if (typeof this !== 'undefined') return this;
  
  throw new Error('Unable to locate global object');
};const theGlobalThis = getGlobalThis();

以上函数并不涵盖全局变量的所有情况。

  1. 如果使用strict,则其值是 undefined
  2. 当我们在 javascript 中形成捆绑包时,通常会在一些可能与此全局代码不同的代码下进行包装。
  3. 在独立的 JavaScript 引擎 shell 环境中,以上代码将不起作用

为了解决上述问题,引入了 globalThis 关键字,该关键字可以在任何环境下随时返回全局对象。

注意:为了保持向后兼容,现在全局对象被认为是 JavaScript 无法消除的错误。它会对性能产生负面影响,并经常使人困惑。

Promise.allSettled()👇

如果你想知道 JavaScript Promise 的用途,请查看此内容 —— JavaScript Promises:简介

Promise 是 JavaScript 向你承诺工作将要完成的方式(如果工作无法完成,则可能会失败)。

新方法会返回一个 Promise ,它会在所有给定的 Promise 均已解决(即已解决或拒绝)之后解决,并带有一系列对象,一个对象描述一个 Promise 的结果。

const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) => setTimeout(reject, 100, 'foo'));
const promises = [promise1, promise2];Promise.allSettled(promises).
  then((results) => results.forEach((result) => console.log(result.status)));
// 预期输出:
// "fulfilled"
// "rejected"

这与 Promise.all 不同,因为 Promise.all 在可迭代对象中的 Promise 被拒绝后就立即拒绝。

下面是当前支持的 promise 方法的比较

Short-circuit? Short-circuits on? Fulfilled on? Rejected on?
Promise.all First rejected promise All promise fulfilled First rejected promise
Promise.allSettled N/A Always N/A
Promise.race First settled First promise fulfilled First rejected promise

动态导入👇

静态与动态导入

这个很疯狂,在我们深入研究它之前,先看看静态导入是什么。

静态导入仅接受字符串文字作为模块说明符,并通过运行前的“链接”过程将绑定引入本地作用域。

静态的 import 语法只能在文件的顶层使用。

import * as module from './utils.mjs';

静态 import 可以启用重要的用例,如静态分析、捆绑工具、和tree-shaking。

但是以下这些:

  • 按需(或有条件)导入模块
  • 在运行时计算模块说明符
  • 从常规脚本(而不是模块)中导入模块

动态导入出现之前是不可能的 — import(moduleSpecifier) 返回所请求模块的模块命名空间对象的promise,它是在提取、实例化和评估模块的所有依赖关系以及模块本身之后才创建的。

<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*。在其他情况下,考虑用动态 *import()*按需加载依赖项。

稳定排序(现在能够得到一致和可靠的结果)👇

稳定在算法意义上的意思是:*它是保留顺序,还是仅保证项目“相等”*?

让我们通过一个例子理解它:

const people = [
  {name: 'Gary', age: 20},
  {name: 'Ann', age: 20},
  {name: 'Bob', age: 17},
  {name: 'Sue', age: 21},
  {name: 'Sam', age: 17},
];
// Sort people by name
people.sort( (p1, p2) => {
  if (p1.name < p2.name) return -1;
  if (p1.name > p2.name) return 1;
  return 0;
});console.log(people.map(p => p.name));
// ['Ann', 'Bob', 'Gary', 'Sam', 'Sue']
// Re-sort people by age
people.sort( (p1, p2) => {
  if (p1.age < p2.age) return -1;
  if (p1.age > p2.age) return 1;
  return 0;
});console.log(people.map(p => p.name));
// 我们期望先按年龄,然后按年龄组中的姓名排序:
// ['Bob', 'Sam', 'Ann', 'Gary', 'Sue']
// 但是我们可能会得到其中的任何一种,这取决于浏览器:
// ['Sam', 'Bob', 'Ann', 'Gary', 'Sue']
// ['Bob', 'Sam', 'Gary', 'Ann', 'Sue']
// ['Sam', 'Bob', 'Gary', 'Ann', 'Sue']

如果你得到的是最后三个结果之一,则可能是你用的是 Google Chrome 浏览器,或者可能是没有将 Array.sort()实现为“稳定”算法的各种浏览器之一。

这是因为不同的 JS 引擎(在不同的浏览器上)采用了不同的路径来实现排序,而且某些 JavaScript 引擎对短数组使用稳定的排序,而对长数组使用不稳定的排序。

这就导致了因为排序稳定性的行为不一致而引发了很多混乱。这就是为什么在开发环境中与排序相关的内容似乎都可以工作,但是在生产环境中,由于和测试排序所使用的数组大小不同,我们开始看到其他内容的原因。

注意:有一些第三方库,我强烈衷心推荐 Lodash,它能够提供稳定的排序

但这些问题已经解决,我们在大多数浏览器上都能得到稳定的排序,同时语法保持不变。

由于本文有很多知识点和需要实际测试的功能,所以我们将在下一篇文章中继续介绍更多的新功能。