天天看點

ECMAScript 2020(ES11) 的新特性總結

ECMAScript 2020(ES11) 的新特性總結

快速通道:

  • ES6、ES7、ES8、ES9、ES10、ES11、ES12、ES13新特性大全

老規矩,先縱覽下 ES2020 的新功能:

  • 動态 import ():按需導入
  • 空值合并運算符:表達式在 ?? 的左側 運算符求值為undefined或null,傳回其右側
  • 可選連結:?.使用者檢測不确定的中間節點
  • BigInt:新基本資料類型,表示任意精度的整數
  • globalThis:浏覽器:window、worker:self、node:global
  • Promise.allSettled:傳回一個在所有給定的promise已被決議或被拒絕後決議的promise,并帶有一個對象數組,每個對象表示對應的promise結果
  • for-in

    結構:用于規範

    for-in

    語句的周遊順序

1、動态 import ()

用了實作按需導入,

import()

是一個類似函數的文法關鍵字,類似super(),它接收一個字元串作為子產品辨別符,并傳回一個 promise

在 ES 2015 定義的子產品文法中,所有子產品導入文法都是靜态聲明的:

import aExport from "./module"
import * as exportName from "./module"
import { export1, export2 as alias2 } from "./module"
import "./module"           

複制

雖然這套文法已經可以滿足絕大多數的導入需求,而且還可以支援實作靜态分析以及樹抖動等一系列重要的功能。但卻無法滿足一些需要動态導入的需求。例如:

  • 需要根據浏覽器相容性有選擇地加載一些支援庫,
  • 在實際需要時才加載某個子產品的代碼,再
  • 隻是單純地希望延遲加載某些子產品來以漸進渲染的方式改進加載體驗

等等這些,在實際工作中也算是比較常見的需求。若沒有動态導入,将難以實作這些需求。雖然我們可以通過建立 script 标簽來動态地導入某些腳本,但這是特定于浏覽器環境的實作方式,也無法直接和現有的子產品文法結合在一起使用,是以隻能作為内部實作機制,但不能直接暴露給子產品的使用者。

但是動态 import () 解決了這個問題。他可以在任何支援該文法的平台中使用,比如 webpack、node 或 浏覽器環境。并且子產品辨別符的格式則是由各平台自行指定,比如 webpack 及 node 支援使用子產品名直接加載 node_modules 中的子產品,而浏覽器支援使用 url 加載遠端子產品。

import('lodash').then(_ => {
    // other
})           

複制

當子產品及其所依賴的其它子產品都被加載并執行完畢後,promise 将進入fulfilled狀态,結果值便是包含該子產品所有導出内容的一個對象:具名導出項被放在該對象的同名屬性中,而預設導出項則放在名為

default

的屬性中,比如有如下子產品 utils,其導入方式如下:

// utils
export default 'hello lxm';
export const x = 11;
export const y = 22;
// 導入
import('a').then(module => {
    console.info(module)
})
// 結果:
{
   default: 'hello lxm'',
   x: 11,
   y: 22,
}           

複制

如果因為子產品不存在或無法通路等問題導緻子產品加載或執行失敗,promise 便會進入rejected狀态,你可以在其中執行一些回退處理。

2、空值合并運算符(?? )

大家可能遇到過,如果一個變量是空,需要給它指派為一個預設值的情況。通常我們會這樣寫:

let num = number || 222           

複制

但是,以上的代碼會有一個 bug。如果

realCount

的值是

,則會被當作取不到其值,會取到

'無法擷取'

這個字元串。如果想要做到這一點,在這之前就隻能使用三元運算符來實作:

let num = (number !== undefined) ? number : 222           

複制

但現在可以使用了

??

運算符了,它隻有當操作符左邊的值是

null

或者

undefined

的時候,才會取操作符右邊的值:

let num = number ?? 222           

複制

ECMAScript 2020(ES11) 的新特性總結

而且該運算符也支援短路特性:

const x = a ?? getDefaultValue()
// 當 `a` 不為 `undefined` 或 `null` 時,`getDefaultValue` 方法不會被執行           

複制

但需要注意一點,該運算符不能與 AND 或 OR 運算符共用,否則會抛出文法異常:

a && b ?? "default"    // SyntaxError           

複制

這種代碼的歧義比較嚴重,在不同人的了解中,可能有的人覺得按

(a && b) ?? "default"

運作是合理的,而另外一些人卻覺得按

a && (b ?? "default")

運作才對,是以在設計該運算符時就幹脆通過文法上的限制來避免了這種情況。如果确實需要在同一個表達式中同時使用它們,那麼使用括号加以區分即可:

(a && b) ?? "default"           

複制

這個操作符的主要設計目的是為了給可選鍊操作符提供一個補充運算符,是以通常是和可選鍊操作符一起使用的:

const x = a?.b ?? 0;           

複制

下面介紹下ES11新增的可選鍊操作符(

?.

3、可選連結

當我們需要嘗試通路某個對象中的屬性或方法而又不确定該對象是否存在時,該文法可以極大的簡化我們的代碼,比如下面這種情況:

const el = document.querySelector(".class-a")
const height = el.clientHeight           

複制

當我們并不知道頁面中是否真的有一個類名為 class-a 的元素,是以在通路

clientHeight

之前為了防止bug産生需要先進行一些判斷:

const height = el ? el.clientHeight : undefined           

複制

上面的寫法雖然可以實作,但是的确有人會覺得麻煩,而使用「可選鍊操作符」 ,就可以将代碼簡化成如下形式:

const height = el?.clientHeight           

複制

下面介紹常用的使用場景:

屬性通路

需要擷取某個對象中的屬性,就都可以使用該文法:

a?.b
a?.[x]           

複制

上面的代碼中,如果 a 為

undefined

null

,則表達式會立即傳回

undefined

,否則傳回所通路屬性的值。也就是說,它們與下面這段代碼是等價的:

a == null ? undefined : a.b
a == null ? undefined : a[x]           

複制

方法調用

在嘗試調用某個方法時,也可以使用該文法:

a?.()           

複制

同樣是如果 a 為

undefined

null

,則傳回

undefined

,否則将調用該方法。不過需要額外注意的是,該操作符并不會判斷 a 是否是函數類型,是以如果 a 是一個其它類型的值,那麼這段代碼依然會在運作時抛出異常。

通路深層次屬性

在通路某個對象較深層級的屬性時,也可以串聯使用該操作符:

a?.b?.[0]?.()?.d           

複制

可能有人會懶得先去判斷是否真的有必要,就給通路鍊路中的每個屬性都加上該操作符。但類似上面代碼中所展示的那樣,這種代碼可讀性比較差。而且若真的有一個應當存在的對象因為某些 bug 導緻它沒有存在,那麼在通路它時就應當是抛出異常,這樣可以及時發現問題,而不是使它被隐藏起來。建議隻在必要的時候才使用可選鍊操作符。

4、BigInt

在 ES 中,所有 Number 類型的值都使用 64 位浮點數格式存儲,是以 Number 類型可以有效表示的最大整數為 2^53。而使用新的 BigInt 類型,可以操作任意精度的整數。

有兩種使用方式:1、在數字字面量的後面添加字尾

n

;2、使用其構造函數

BigInt

const bigInt = 9007199254740993n
const bigInt = BigInt(9007199254740992)           

複制

// 在超過 Number 最大整數限制時,我們也可以改為傳入一個可能被正确解析的字元串

const bigInt = BigInt(‘9007199254740993’)

和 Number 類似,BigInt 也支援

+

-

、、

**

%

運算符:

3n + 2n    // => 5n

3n  2n    // => 6n

3n  2n   // => 9n

3n % 2n    // => 1n           

複制

但因為 BigInt 是純粹的整數類型,無法表示小數位,是以 BigInt 的除法運算(

/

)的結果值依然還是一個整數,即向下取整:

const bigInt = 3n;

bigInt / 2n;    // => 1n,而不是 1.5n           

複制

ECMAScript 2020(ES11) 的新特性總結

同樣也位支援位運算符,除了無符号右移運算符:

1n & 3n    // => 1n

1n | 3n    // => 3n

1n ^ 3n    // => 2n

~1n        // => -2n

1n << 3n   // => 8n

1n >> 3n   // => 0n



1n >>> 3n  // Uncaught TypeError: BigInts have no unsigned right shift, use >> instead           

複制

ECMAScript 2020(ES11) 的新特性總結

BigInt 可以和字元串之間使用

+

運算符連接配接

1n + ’ Number’   // => 1 Number

'Number ’ + 2n   // => Number 2           

複制

下面這些場景不支援使用BigInt:

1、BigInt 無法和 Number 一起運算,會抛出類型異常

1n + 1

// Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversions           

複制

2、一些内置子產品如 Math 也不支援 BigInt,同樣會抛出異常

Math.pow(2n, 64n)

// Uncaught TypeError: Cannot convert a BigInt value to a number           

複制

3、BigInt 和 Number 相等,但并不嚴格相等,但他們之間可以比較大小

1n  1    // => true

1n = 1   // => false           

複制

但他們之間是可以比較大小的:

1n < 2     // => true

1n < 1     // => false



2n > 1     // => true

2n > 2     // => false           

複制

而且在轉換為 Boolean 值時,也和 Number 一樣,

0n

轉為

false

,其它值轉為

true

!!0n       // => false

!!1n       // => true           

複制

另外兩者之間隻能使用對方的構造函數進行轉換:

Number(1n) // => 1

BigInt(1)  // => 1n           

複制

ECMAScript 2020(ES11) 的新特性總結

但兩者之間的轉換也都有一些邊界問題:

// 當 BigInt 值的精度超出 Number 類型可表示的範圍時,會出現精度丢失的問題

Number(9007199254740993n)

// => 9007199254740992



// 當 Number 值中有小數位時,BigInt 會抛出異常

BigInt(1.1)

// VM4854:1 Uncaught RangeError: The number 1.1 cannot be converted to a BigInt because it is not an integer           

複制

ECMAScript 2020(ES11) 的新特性總結

配套地,在類型化數組中也提供了與 BigInt 對應的兩個數組類型:

BigInt64Array

BigUint64Array

const array = new BigInt64Array(4);

array[0]   // => 0n



array[0] = 2n

array[0]   // => 2n           

複制

但因為每個元素限定隻有 64 位,是以即便使用無符号類型,最大也隻能表示 2^64 - 1:

const array = new BigUint64Array(4);

array[0] = 2n  64n

array[0]   // => 0n



array[0] = 2n ** 64n - 1n

array[0]   // => 18446744073709551615n           

複制

ECMAScript 2020(ES11) 的新特性總結

5、globalThis

浏覽器:window、worker:self、node:global

在浏覽器環境中,我們可以有多種方式通路到全局對象,最常用到的肯定是

window

,但除此之外還有

self

,以及在特殊場景下使用的

frames

paraent

以及

top

我們通常不怎麼需要關心

window

self

之間的差別,但如果使用 Web Worker,那就應當了解

window

是隻在主線程中才有的全局屬性,在 Worker 線程中,我們需要改為使用

self

而在 node.js 環境中,我們需要使用

global

,至于像 JSC.js 這種更小衆的環境中,則需要使用

this

在一般的開發工作中,可能很少需要通路全局環境,而且大多時候也隻需要基于一種環境進行開發,是以不太需要處理這種麻煩的問題。但是對于 es6-shim 這種需要支援多種環境的基礎庫來說,它們需要解決這個問題。

早先,我們可以通過下面這段代碼較為友善地拿到全局對象:

const globals = (new Function(‘return this;’))()           

複制

但受到 Chrome APP 内容安全政策的影響(為緩解跨站腳本攻擊的問題,該政策要求禁止使用 eval 及相關的功能),上面這段代碼将無法在 Chrome APP 的運作環境中正常執行。

無奈之下,像

es6-shim

這種庫就隻能窮舉所有可能的全局屬性:

var getGlobal = function () {

// the only reliable means to get the global object is

    // Function('return this')()

    // However, this causes CSP violations in Chrome apps.

    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’);

};

var globals = getGlobal();

if (!globals.Reflect) {

defineProperty(globals, ‘Reflect’, {}, true);

}           

複制

這種問題等真的遇到了,每次處理起來也是很麻煩的。是以才有了這次提案中的

globalThis

通過

globalThis

,我們終于可以使用一種标準的方法拿到全局對象,而不用關心代碼的運作環境。對于

es6-shim

這種庫來說,這是一個極大的便利特性:

if (!globalThis.Reflect) {

defineProperty(globalThis, ‘Reflect’, {}, true);

}           

複制

另外,關于

globalThis

還有一些細節的問題,比如為滿足 Secure ECMAScript 的要求,

globalThis

是可寫的。而在浏覽器頁面中,受到 outer window 特性的影響,

globalThis

實際指向的是

WindowProxy

,而不是目前頁面内真實的全局對象(該對象不能被直接通路)。

6、Promise.allSettled

Promise

上有提供一組組合方法(比如最常用到的

Promise.all

),它們都是接收多個 promise 對象,并傳回一個表示組合結果的新的 promise,依據所傳入 promise 的結果狀态,組合後的 promise 将切換為不同的狀态。

目前為止這類方法一共有如下四個,這四個方法之間僅有判斷邏輯上的差別,也都有各自所适用的場景:

  • Promise.all

    傳回一個組合後的 promise,當所有 promise 全部切換為 fulfilled 狀态後,該 promise 切換為 fulfilled 狀态;但若有任意一個 promise 切換為 rejected 狀态,該 promise 将立即切換為 rejected 狀态;
  • Promise.race

    傳回一個組合後的 promise,當 promise 中有任意一個切換為 fulfilled 或 rejected 狀态時,該 promise 将立即切換為相同狀态;
  • Promise.allSettled

    傳回一個組合後的 promise,當所有 promise 全部切換為 fulfilled 或 rejected 狀态時,該 promise 将切換為 fulfilled 狀态;
  • Promise.any

    傳回一個組合後的 promise,當 promise 中有任意一個切換為 fulfilled 狀态時,該 promise 将立即切換為 fulfilled 狀态,但隻有所有 promise 全部切換為 rejected 狀态時,該 promise 才切換為 rejected 狀态。(ECMAScript2021 )

Promise.allSettled用法:

傳入一個數組,裡面放任意多個 promise 對象,并接受一個表示組合結果的新的 promise。

需要注意的是,組合後的 promise 會等待所有所傳入的 promise,當它們全部切換狀态後(無論是 fulfilled 狀态 還是 rejected 狀态),這個組合後的 promise 會切換到 fulfilled 狀态并給出所有 promise 的結果資訊:

async function a() {

const promiseA = fetch(’/api/a’)    // => rejected,  <Error: a>

    const promiseB = fetch(’/api/B’)    // => fulfilled, “b”



const results = await Promise.allSettled([ promiseA, promiseB])

results.length   // => 3

    results[0]       // => { status: “rejected”, reason: <Error: a> }

    results[1]       // => { status: “fulfilled”, value: “b” }

}           

複制

因為結果值是一個數組,是以你可以很容易地過濾出任何你感興趣的結果資訊:

// 擷取所有 fulfilled 狀态的結果資訊

results.filter( result => result.status = “fulfilled” )

// 擷取所有 rejected 狀态的結果資訊

results.filter( result => result.status = “rejected” )

// 擷取第一個 rejected 狀态的結果資訊

results.find( result => result.status = “rejected” )           

複制

使用場景如下:

1、有時候在進行一個頁面的初始化流程時,需要加載多份初始化資料,或執行一些其它初始化操作,而且通常會希望等待這些初始化操作全部完成之後再執行後續流程:

async function init() {

setInited(false)

setInitError(undefined)

const results = await Promise.allSettled([

loadDetail(),

loadRecommentListFirstPage(),

initSDK(),

])

const errors = results

.filter( result => result.status = “rejected” )

.map( rejectedResult => rejectedResult.reason )

if (errors.length) {

setInitError(errors[0])

$logs.error(errors)

}

setInited(true)}           

複制

2、又例如我們有自定義的全局消息中心,那麼還可以基于

allSettled

作一些異步支援的事情。比如在打開登入彈出層并在使用者成功登入後,向頁面中廣播一個

login

事件,通常頁面中其它地方監聽到該事件後需要向服務端請求新的資料,此時我們可能需要等待資料全部更新完畢之後再關閉登入彈出層:

async function login() {

// goto login …



const results = messageCenter.login.emit()

const promiseResults = results.filter(isPromise)

if (promiseResults.length) {

await Promise.allSettled(promiseResults)

}

closeLoginModal()

closeLoading()}           

複制

7、for-in 結構

用于規範

for-in

語句的周遊順序

在之前的 ES 規範中幾乎沒有指定

for-in

語句在周遊時的順序,但各 ES 引擎的實作在大多數情況下都是趨于一緻的,隻有在一些邊界情況時才會有所差别。我們很難能夠要求各引擎做到完全一緻,主要原因在于

for-in

是 ES 中所有周遊 API 中最複雜的一個,再加上規範的疏漏,導緻各大浏覽器在實作該 API 時都有很多自己特有的實作邏輯,各引擎的維護人員很難有意願去重新審查這部分的代碼。

是以規範的作者作了大量的工作,去測試了很多現有的 ES 引擎中

for-in

的周遊邏輯。并梳理出了它們之間一緻的部分,然後将這部分補充到了 ES 規範 當中。

另外,規範中還提供了一份示例代碼,以供各引擎在實作

for-in

邏輯時參考使用,大家可以看一下:

function* EnumerateObjectProperties(obj) {

const visited = new Set();

for (const key of Reflect.ownKeys(obj)) {

if (typeof key = “symbol”) continue;

const desc = Reflect.getOwnPropertyDescriptor(obj, key);

if (desc) {

visited.add(key);

if (desc.enumerable) yield key;

}

}

const proto = Reflect.getPrototypeOf(obj);

if (proto === null) return;

for (const protoKey of EnumerateObjectProperties(proto)) {

if (!visited.has(protoKey)) yield protoKey; }}           

複制

本文參考:

  • ECMAScript 2020 的新功能
  • JS文法 ES6、ES7、ES8、ES9、ES10、ES11、ES12新特性
  • ECMAScript 2020 簡介