阅读 1645

ES2019、ES2020、ES2021、ES2022 特性大汇总【2021-10-01更新】

[本文持续更新,收藏不亏,最近一次于 2021-10-01 更新]

这周我在看 snabbdom 的源码,然后本来计划更新虚拟 DOM 的 Diff 算法原理,但是一切的计划都被昨天的线上问题打乱了,然后还要写 OKR,来一把刀插在我胸口吧,那都比现在痛快!所以还是更新一点轻松的知识吧。苦笑脸 :)

另外推荐大家使用浏览器阅读,掘金的主题 「channing-cyan」真的很好看。

在工作中,我们最常用的特性基本都包含在 ES 2015(ES 6)那个版本里面了,事实上,最近几年来,有很多实用的新语法加入到了 JavaScript 中来,今天我们从 ES 2019 开始,按照时间的顺序,一齐打包把他们都介绍了。

ps: ES 2022 要发布的特性还没有完全确定,故 ES 2022 介绍的内容是截止自今天(2021年7月18日)。

ES 2019

1. Optional catch binding

以前我们写 try...catch 需要这么写,不管我们需不需要变量 e,都得写上:

try {
   ...
} catch(e) {
    console.log(e)
}
复制代码

现在,catch 绑定变量是可选的了,可以简写成:

try {
   ...
} catch {
    // catch 简写了
}
复制代码

说到这里,吐槽一句支付宝小程序,这个语法都还不支持。

2. Symbol.prototype.description

const s = Symbol('foo');
console.log(s.description); // foo 
const s1 = Symbol();
console.log(s1.description); // undefined
复制代码

这个属性是只读的,不存在的话会返回 undefined

3. Function.prototype.toString

以前此方法的返回值没有一个标准,完全按照浏览器厂商的喜好来,现在标准要求返回函数的源代码:

function foo() {
    console.log('hi')
}

foo.toString() // "function foo() {console.log('hi')}"
复制代码

4. Object.fromEntries

此方法接受一个实现了 iterable 接口的对象作为入参,比如 Map 、Array。您也可以让自己的自定义对象实现 iterable 接口,同样可以使用此方法。

Object.fromEntriesObject.entrie 作用正好是相反的,把它俩放在一块看一下:

const entries = new Map([
  ['key1', 'val1'],
  ['key2', 'val2']
])
const obj = Object.fromEntries(entries)
console.log(obj)// {key1: "val1", key2: "val2"}
const map = Object.entries(obj)
console.log(map) // 和 entries 的值一样
复制代码

5. Array.prototype.flat

它在不影响原数组的基础上,返回一个「拍平」了的新数组:

[1, 2, [3]].flat() // [1, 2, 3]
复制代码

它还可以接受一个入参,指定深度是多少,不传的话,深度默认是 1,于是乎,就会产生下面这样的结果:

[1, 2, [3, [4, 5]]].flat() // [1, 2, 3, [4, 5]]]
[1, 2, [3, [4, 5]]].flat(2) // [1, 2, 3, 4, 5]
复制代码

如果你是个猛男,不管多少层,都拍成一维的,可以直接入参 Infinity

[1, 2, [3, [4, 5, [6, 7]]]].flat(Infinity) // [1, 2, 3, 4, 5, 6, 7]
复制代码

6. Array.prototype.flatMap

人如其名,它的效果就是 Array.flat + Array.map,理解它的最好方式就是举个例子:

let arr = [1, 2, 3, 4];
arr.flatMap(x => [x*2]) // [2, 4, 6, 8]

// 最后的结果就相当于
arr.map(x => [x*2]).flat() // [2, 4, 6, 8]
复制代码

虽然得到的结果是一样的,但是这个函数实现方式要比 map + flat 的方式要高效,因为写成一个方法,我们可以只遍历一遍就做到这件事。

7. String.prototype.trimStart & String.prototype.trimEnd

这个是比较简单的,直接看示例就好:

const str = '    Hello World    '
str.trimStart() // "Hello World    "
str.trimEnd() // "    Hello World"
复制代码

ES 2020

8. String.prototype.matchAll

这个函数入参的类型是正则,返回的是一个实现 iterator 接口的对象,我们比较一下它和 String.prototype.match 的用法:

image.png

9. 动态 import 引入

以前限于 ES Module 的实现原理,是不支持动态引入的,我有一篇旧文,涉及到了它的一部分原理,但是现在可以了:

// filename: a.js
export function hello() {
   console.log('Hello World!')
}

import('./a.js').then(module => {
    console.log(module.hello()); // Hello World !
})
复制代码

10. BigInt

以前在 JavaScript 中,最大的安全整数是 2^53 - 1,所以会有下面的现象:

image.png

如果我们现在使用 BigInt 去存储就不会有这样的问题了:

image.png

11. Promise.allSettled

它和 Promise.all 的功能比较相近,但是 Promise.all 有短路原则,会在一个 Promise 对象进入 rejected 态后就结束了。但是 Promise.allSettled 没有任何短路原则,都解决了才会结束。

var myPromiseArray = [Promise.resolve(), Promise.reject(new Error())]
Promise.all(myPromiseArray).then(
  res => console.log(res),
  err => console.error('出现了一些错误'),
);

复制代码

image.png

但是,Promise.allSettled 的表现如下:

Promise.allSettled(myPromiseArray).then(
  res => console.log(res),
  err => console.error('出现了一些错误'),
);
复制代码

image.png

12. globalThis

在浏览器中,window 代指全局对象,Node 环境中,global 代指全局对象,webWorker 中,self 代指全局对象......

如果我们的代码需要在不同的平台运行,为了获取全局变量,就得考虑各个平台下的兼容性,就得做一些比较麻烦的判断,就比如在 Underscore.js 的源码中,就有这么一段折磨人的代码:

var root = (typeof self == 'object' && self.self == self && self) ||
           (typeof global == 'object' && global.global == global && global) ||
           this;
复制代码

这就是引入 globalThis 的目的,现在可以一个变量走天下了。

13. Optional Chaining(a?.value)

var fooValue = myForm.querySelector('input[name=foo]')?.value
复制代码

就相当于原来的:

var fooInput = myForm.querySelector('input[name=foo]')
var fooValue = fooInput ? fooInput.value : undefined
复制代码

这个特性真的好评。

同时,在函数中也可以用,比如我们想调用 foo 这个函数,但是 foo 可能是 null 或者 undefined,以前的话,我们可能这样子做:

if (foo) {
    foo()
}
复制代码

但是现在我们有了更方便的写法:

foo?.();
复制代码

14. Nullish coalescing Operator(??)

只有在值为 null 或者 undefined 的时候才取后面的值。具体请看下面的示例:

image.png

15. import.meta

// filename: index.html
<script type="module" src="path/to/some.js"></script>

// filename: index.js
console.log(import.meta); // { url: "file://path/to/some.js" }


// filename: index.mjs
import './index2.mjs?someURLInfo=5';

// filename: index2.mjs
new URL(import.meta.url).searchParams.get('someURLInfo'); // 5
复制代码

虽然我知道语法,但是对它的用途还不是特别理解。


ES 2021

16. String.prototype.replaceAll

之前,如果我们想取代一个字符串所有的字符,一般使用正则去做:

const queryString = 'q=query+string+parameters';
const withSpaces = queryString.replace(/+/g, ' ');
复制代码

现在有了此方法,我们可以:

const queryString = 'q=query+string+parameters';
const withSpaces = queryString.replaceAll('+', ' ');
复制代码

17. Promise.any

这个可以和之前的 Promise.race 进行比较,二者都有短路原则,Promise.race 会在某一个 Promise 被解决的时候完成,而 Promise.any 会在某一个 Promise 处于 fulfilled 态的时候完成。有点绕,还是举例子:

var promise1 = new Promise((resolve, reject)=> {
  setTimeout(() => {
    reject('rejected')
  }, 100)
})

var promise2 = new Promise((resolve, reject)=> {
  setTimeout(() => {
    resolve('resolved')
  }, 200)
})
复制代码

首先,看 promise.race 的结果:

Promise.race([promise1, promise2]).then(res => {
  console.log(res)
}, (error) => {
  console.error(error)
})

复制代码

image.png

接下来,看 Promise.any 的结果:

Promise.any([promise1, promise2]).then(res => {
  console.log('我没有错误')
  console.log(res)
}, (error) => {
  console.error(error)
})
复制代码

image.png

18. WeakRefs

最简单的使用就是像下面这样:

var foo = () => {console.log('hi')};
var weakFoo = new WeakRef(foo);
console.log(weakFoo.deref()) // () => {console.log('hi')}
复制代码

WeakRef 的入参是一个对象,它会创建对这个对象的弱引用,并且不会阻止这个对象的垃圾回收。但是鉴于浏览器的垃圾回收机制并不统一,所以它的表现可能也会因为浏览器的不同而不同,建议是能别用就别用。

weakFoo.deref 用来取得指向的原对象,如果还没有被回收,就返回原对象,回收了就返回 undefined

19. Logical Assignment Operators

x &&= y  // 相当于 x && (x = y)

x ||= y  // 相当于 x || (x = y)

x ??= y  // 相当于 x ?? (x = y)
复制代码

个人觉得第 1、3 个很实用。

20. Numeric separators

现在数字有了更方便阅读的书写方式:

const a = 1_000 
console.log(a) // 1000

const b = 1_000_000
console.log(b) // 1000000
复制代码

ES 2022

21. Class Fields

历尽艰辛,私有类的提案终于在这个版本被通过了,里面包括了私有变量、私有方法、静态私有变量、静态私有方法四个。

设置私有属性的方式是在变量前面加一个修饰符 #:

class ClassWithPrivateProperty {
  #privateField;
  static #PRIVATE_STATIC_FIELD;

  constructor() {
    this.#privateField = 42;
  }

  #privateMethod() {
    return 'hello world';
  }

  static #privateStaticMethod() {
    return 'hello world';
  }
}
复制代码

值得注意的是,在当前版本的 TypeScript 中,我们的私有属性使用 private 这一个修饰符,但是这个修饰符只会在我们写代码的时候限制我们,真正转译成 JavaScript 代码的时候,还是公共属性。

22. RegExp Match Indices

以前我们的正则表达式后面可以加 ig 去修饰,i 代表忽略大小写,g 代表全局匹配,现在又加了一个 d

const re1 = /a+(?<Z>z)?/d;

const s1 = "xaaaz";
const m1 = re1.exec(s1);
m1.indices[0][0] === 1;
m1.indices[0][1] === 5;
s1.slice(...m1.indices[0]) === "aaaz";
复制代码

image.png

re1 这个正则表达式中,有个 (?<Z>z) 的写法,它是什么意思呢?它会匹配 z 这个字符,并把匹配到的结果放到 groups 这个对象的 Z 属性里。更多资料请参阅Groups_and_Ranges

23. Top-level await

以前我们使用 await 时,必须要放到 async 函数里,这就限制了一些场景,比如我们在全局作用域使用 import 的异步加载方式。而这个特性就是为这些场景提供了便利:

let jQuery;
try {
  jQuery = await import('https://cdn-a.com/jQuery');
} catch {
  jQuery = await import('https://cdn-b.com/jQuery');
}
复制代码
const strings = await import(`/i18n/${navigator.language}`);
复制代码

24. Ergonomic brand checks for Private Fields

支持了使用 in 去判断私有属性在对象里面存不存在。拿例子说话比较好:

class C {
  #brand;

  #method() {}

  get #getter() {}

  static isC(obj) {
    return #brand in obj && #method in obj && #getter in obj;
  }
}
复制代码

那它的应用场景是什么呢?

假设我们有一个创造类的工厂方法,并创造了两个类:

function createClass() {
  return class {
    #name;
    constructor(name) {
      this.#name = name;
    }
    static getName(inst) {
      return inst.#name;
    }
  };
}

const Class1 = createClass()
const Class2 = createClass()
复制代码

这两个类虽然函数的属性是一样的,但是每一个类的私有属性都是独立的,如果我们这样使用就会报错:

Class1.getName(new Class2('小明'))
复制代码

image.png

为了兼容这种场景,就有了这个提案,我们可以像下面这解决:

function createClass() {
  return class {
    #name;
    constructor(name) {
      this.#name = name;
    }
    static getName(inst) {
      if (#name in inst) {
          return inst.#name;
      }
      return undefined;
    }
  };
}
复制代码

25. .at()

这个就是取数组索引的:

var a = [1, 2, 3];
a.at(1) // 2
a.at(-1) // 3
复制代码

因为 JavaScript 数组的特殊性(一个特殊的对象),我们无法通过 a[-1] 这种形式取到数组的倒数第一项,所以有了这个语法。

26. Accessible Object.prototype.hasOwnProperty`

因为 JS 并没有保护叫做 'hasOwnProperty' 的属性名,为了防止意外的产生,现在 Eslint 一般都会默认开启不允许在对象上判断 hasOwnProperty 这条校验。

除此之外,我们有一个对象没有原型,也就不能调用 hasOwnProperty 方法了:

Object.create(null).hasOwnProperty("foo")
// Uncaught TypeError: Object.create(...).hasOwnProperty is not a function
复制代码

于是乎,我们一般都这样来判断:

let hasOwnProperty = Object.prototype.hasOwnProperty

if (hasOwnProperty.call(object, "foo")) {
  console.log("has property foo")
}
复制代码

我自己在项目中也是这么写的,但是大家都这么写,JS 就觉得可以考虑出一个公共方法了,就有了 hasOwn 方法,可以比较大的简化我们上面的写法:

if (Object.hasOwn(object, "foo")) {
  console.log("has property foo")
}
复制代码

27. Class Static Block

以前,我们初始化类的静态成员变量只能在定义的时候去做,不能放到构造函数里面(静态方法不用实例化,就不用调用构造函数了)。

现在,我们可以在类内部开辟一个专门为静态成员初始化的作用域,这对一些比较复杂的场景很适用:

在没有作用域之前,我们可能会使用一个工具函数去初始化:

class Translator {
  static translations = {
    yes: 'ja',
    no: 'nein',
    maybe: 'vielleicht',
  };
  static englishWords = [];
  static germanWords = [];
  static _ = initializeTranslator(); // (A)
}
function initializeTranslator() {
  for (const [english, german] of Object.entries(Translator.translations)) {
    Translator.englishWords.push(english);
    Translator.germanWords.push(german);
  }
}
复制代码

有了类的静态作用域之后,我们就可以按如下的格式了:

class Translator {
  static translations = {
    yes: 'ja',
    no: 'nein',
    maybe: 'vielleicht',
  };
  static englishWords = [];
  static germanWords = [];
  static { // (A)
    for (const [english, german] of Object.entries(translations)) {
      this.englishWords.push(english);
      this.germanWords.push(german);
    }
  }
}
复制代码

到这里,除却一些优化 API 的提案(数组排序优化、for...in 顺序等),我已经把 ES2019 - 2022 通过的大部分提案都介绍了,后续有了新的,我也会补充到这个文章里来。

谢谢阅读 ~

参考文章

finished-proposals

文章分类
前端
文章标签