ES11來了,還學得動嗎?

寫在前面

ES2020(即 ES11)上週(2020 年 6 月)已經正式發佈,在此以前進入 Stage 4 的 10 項提案均已歸入規範,成爲 JavaScript 語言的新特性html

一.特性一覽

ES Module 迎來了一些加強:前端

正式支持了安全的鏈式操做:git

提供了大數運算的原生支持:es6

一些基礎 API 也有了新的變化:github

  • Promise.allSettled:一個新的 Promise 組合器,不像allrace同樣具備短路特性
  • String.prototype.matchAll:以迭代器的形式返回全局匹配模式下的正則表達式匹配到的全部結果(indexgroups等)
  • globalThis:訪問全局做用域this的通用方法
  • for-in mechanics:規範for-in循環的某些行爲

二.ES Module 加強

動態 import

咱們知道ES Module是一套靜態的模塊系統正則表達式

The existing syntactic forms for importing modules are static declarations.

靜態體如今:數組

They accept a string literal as the module specifier, and introduce bindings into the local scope via a pre-runtime "linking" process.
  • 靜態加載:import/export聲明只能出如今頂層做用域,不支持按需加載、懶加載
  • 靜態標識:模塊標識只能是字符串字面量,不支持運行時動態計算而來的模塊名

例如:promise

if (Math.random()) {
    import 'foo'; // SyntaxError
}

// You can’t even nest `import` and `export`
// inside a simple block:
{
    import 'foo'; // SyntaxError
}

這種嚴格的靜態模塊機制讓基於源碼的靜態分析、編譯優化有了更大的發揮空間:瀏覽器

This is a great design for the 90% case, and supports important use cases such as static analysis, bundling tools, and tree shaking.

但對另外一些場景很不友好,好比:安全

  • 苛求首屏性能的場景:經過import聲明引用的全部模塊(包括初始化暫時用不到的模塊)都會在初始化階段前置加載,影響首屏性能
  • 難以提早肯定目標模塊標識的場景:例如根據用戶的語言選項動態加載不一樣的模塊(module-enmodule-zh等)
  • 僅在特殊狀況下才須要加載某些模塊的場景:例如異常狀況下加載降級模塊

爲了知足這些須要動態加載模塊的場景,ES2020 推出了動態 import 特性(import()):

import(specifier)

import()「函數」輸入模塊標識specifier(其解析規則與import聲明相同),輸出Promise,例如:

// 目標模塊  ./lib/my-math.js
function times(a, b) {
  return a * b;
}
export function square(x) {
  return times(x, x);
}
export const LIGHTSPEED = 299792458;

// 當前模塊 index.js
const dir = './lib/';
const moduleSpecifier = dir + 'my-math.mjs';

async function loadConstant() {
  const myMath = await import(moduleSpecifier);
  const result = myMath.LIGHTSPEED;
  assert.equal(result, 299792458);
  return result;
}
// 或者不用 async & await
function loadConstant() {
  return import(moduleSpecifier)
  .then(myMath => {
    const result = myMath.LIGHTSPEED;
    assert.equal(result, 299792458);
    return result;
  });
}

import聲明相比,import()特色以下:

  • 可以在函數、分支等非頂層做用域使用,按需加載、懶加載都不是問題
  • 模塊標識支持變量傳入,可動態計算肯定模塊標識
  • 不只限於module,在普通的script中也能使用

注意,雖然長的像函數,但import()其實是個操做符,由於操做符可以攜帶當前模塊相關信息(用來解析模塊表示),而函數不能:

Even though it works much like a function, import() is an operator: in order to resolve module specifiers relatively to the current module, it needs to know from which module it is invoked. A normal function cannot receive this information as implicitly as an operator can. It would need, for example, a parameter.

import.meta

另外一個 ES Module 新特性是import.meta,用來透出模塊特定的元信息:

import.meta, a host-populated object available in Modules that may contain contextual information about the Module.

好比:

  • 模塊的 URL 或文件名:例如 Node.js 裏的__dirname__filename
  • 所處的script標籤:例如瀏覽器支持的document.currentScript
  • 入口模塊:例如 Node.js 裏的process.mainModule

諸如此類的元信息均可以掛到import.meta屬性上,例如:

// 模塊的 URL(瀏覽器環境)
import.meta.url
// 當前模塊所處的 script 標籤
import.meta.scriptElement

須要注意的是,規範並無明肯定義具體的屬性名和含義,都由具體實現來定,因此特性提案裏的但願瀏覽器支持的這兩個屬性未來可能支持也可能不支持

P.S.import.meta自己是個對象,原型爲null

export-ns-from

第三個 ES Module 相關的新特性是另外一種模塊導出語法:

export * as ns from "mod";

同屬於export ... from ...形式的聚合導出,做用上相似於:

import * as ns from "mod";
export {ns};

但不會在當前模塊做用域引入目標模塊的各個 API 變量

P.S.對照import * as ns from "mod";語法,看起來像是 ES6 模塊設計中排列組合的一個疏漏;)

三.鏈式操做支持

Optional Chaining

至關實用的一個特性,用來替代諸如此類冗長的安全鏈式操做:

const street = user && user.address && user.address.street;

可換用新特性(?.):

const street = user?.address?.street;

語法格式以下:

obj?.prop     // 訪問可選的靜態屬性
// 等價於
(obj !== undefined && obj !== null) ? obj.prop : undefined

obj?.[«expr»] // 訪問可選的動態屬性
// 等價於
(obj !== undefined && obj !== null) ? obj[«expr»] : undefined

func?.(«arg0», «arg1») // 調用可選的函數或方法
// 等價於
(func !== undefined && func !== null) ? func(arg0, arg1) : undefined

P.S.注意操做符是?.而不是單?,在函數調用中有些奇怪alert?.(),這是爲了與三目運算符中的?區分開

機制很是簡單,若是出如今問號前的值不是undefinednull,才執行問號後的操做,不然返回undefined

一樣具備短路特性:

// 在 .b?.m 時短路返回了 undefined,而不會 alert 'here'
({a: 1})?.a?.b?.m?.(alert('here'))

&&相比,新的?.操做符更適合安全進行鏈式操做的場景,由於:

  • 語義更明確:?.遇到屬性/方法不存在就返回undefined,而不像&&同樣返回左側的值(幾乎沒什麼用)
  • 存在性判斷更準確:?.只針對nullundefined,而&&遇到任意假值都會返回,有時沒法知足須要

例如經常使用的正則提取目標串,語法描述至關簡潔:

'string'.match(/(sing)/)?.[1] // undefined
// 以前須要這樣作
('string'.match(/(sing)/) || [])[1] // undefined

還能夠配合 Nullish coalescing Operator 特性填充默認值:

'string'.match(/(sing)/)?.[1] ?? '' // ''
// 以前須要這樣作
('string'.match(/(sing)/) || [])[1] || '' // ''
// 或者
('string'.match(/(sing)/) || [, ''])[1] // ''

Nullish coalescing Operator

一樣引入了一種新的語法結構(??):

actualValue ?? defaultValue
// 等價於
actualValue !== undefined && actualValue !== null ? actualValue : defaultValue

用來提供默認值,當左側的actualValueundefinednull時,返回右側的defaultValue,不然返回左側actualValue

相似於||,主要區別在於??只針對nullundefined,而||遇到任一假值都會返回右側的默認值

四.大數運算

新增了一種基礎類型,叫BigInt,提供大整數運算支持:

BigInt is a new primitive that provides a way to represent whole numbers larger than 2^53, which is the largest number Javascript can reliably represent with the Number primitive.

BigInt

JavaScript 中Number類型所能準確表示的最大整數是2^53,不支持對更大的數進行運算:

const x = Number.MAX_SAFE_INTEGER;
// 9007199254740991 即 2^53 - 1
const y = x + 1;
// 9007199254740992 正確
const z = x + 2
// 9007199254740992 錯了,沒變

P.S.至於爲何是 2 的 53 次方,是由於 JS 中數值都以 64 位浮點數形式存放,刨去 1 個符號位,11 個指數位(科學計數法中的指數),剩餘的 52 位用來存放數值,2 的 53 次方對應的這 52 位所有爲 0,能表示的下一個數是2^53 + 2,中間的2^53 + 1沒法表示:

[caption id="attachment_2213" align="alignnone" width="625"]<img src="http://www.ayqy.net/cms/wordpress/wp-content/uploads/2020/06/JavaScript-Max-Safe-Integer-1024x518.jpg" alt="JavaScript Max Safe Integer" width="625" height="316" class="size-large wp-image-2213" /> JavaScript Max Safe Integer[/caption]

具體解釋見BigInts in JavaScript: A case study in TC39

BigInt類型的出現正是爲了解決此類問題:

9007199254740991n + 2n
// 9007199254740993n 正確

引入的新東西包括:

  • 大整數字面量:給數字後綴一個n表示大整數,例如9007199254740993n0xFFn(二進制、八進制、十進制、十六進制字面量統統能夠後綴個n變成BigInt
  • bigint基礎類型:typeof 1n === 'bigint'
  • 類型構造函數:BigInt
  • 重載數學運算符(加減乘除等):支持大整數運算

例如:

// 建立一個 BigInt
9007199254740993n
// 或者
BigInt(9007199254740993)

// 乘法運算
9007199254740993n * 2n
// 冪運算
9007199254740993n ** 2n
// 比較運算
0n === 0  // false
0n === 0n // true
// toString
123n.toString() === '123'

P.S.關於 BigInt API 細節的更多信息,見ECMAScript feature: BigInt – arbitrary precision integers

須要注意的是BigInt不能與Number混用進行運算

9007199254740993n * 2
// 報錯 Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversions

而且BigInt只能表示整數,因此除法直接取整(至關於Math.trunc()):

3n / 2n === 1n

五.基礎 API

基礎 API 也有一些新的變化,包括 Promise、字符串正則匹配、for-in循環等

Promise.allSettled

Promise.allPromise.race以後,Promise新增了一個靜態方法叫allSettled

// 傳入的全部 promise 都有結果(從 pending 狀態變成 fulfilled 或 rejected)以後,觸發 onFulfilled
Promise.allSettled([promise1, promise2]).then(onFulfilled);

P.S.另外,any也在路上了,目前(2020/6/21)處於 Stage 3

相似於all,但不會由於某些項rejected而短路,也就是說,allSettled會等到全部項都有結果(不管成功失敗)後才進入Promise鏈的下一環(因此它必定會變成 Fulfilled 狀態):

A common use case for this combinator is wanting to take an action after multiple requests have completed, regardless of their success or failure.

例如:

Promise.allSettled([Promise.reject('No way'), Promise.resolve('Here')])
  .then(results => {
    console.log(results);
    // [
    //   {status: "rejected", reason: "No way"},
    //   {status: "fulfilled", value: "Here"}
    // ]
  }, error => {
    // No error can get here!
  })

String.prototype.matchAll

字符串處理的一個常見場景是想要匹配出字符串中的全部目標子串,例如:

const str = 'es2015/es6 es2016/es7 es2020/es11';
str.match(/(es\d+)\/es(\d+)/g)
// 順利獲得 ["es2015/es6", "es2016/es7", "es2020/es11"]

match()方法中,正則表達式所匹配到的多個結果會被打包成數組返回,但沒法得知每一個匹配除結果以外的相關信息,好比捕獲到的子串,匹配到的index位置等:

This is a bit of a messy way to obtain the desired information on all matches.

此時只能求助於最強大的exec

const str = 'es2015/es6 es2016/es7 es2020/es11';
const reg = /(es\d+)\/es(\d+)/g;
let matched;
let formatted = [];
while (matched = reg.exec(str)) {
  formatted.push(`${matched[1]} alias v${matched[2]}`);
}
console.log(formatted);
// 獲得 ["es2015 alias v6", "es2016 alias v7", "es2020 alias v11"]

而 ES2020 新增的matchAll()方法就是針對此類種場景的補充:

const results = 'es2015/es6 es2016/es7 es2020/es11'.matchAll(/(es\d+)\/es(\d+)/g);
// 轉數組處理
Array.from(results).map(r => `${r[1]} alias v${r[2]}`);
// 或者從迭代器中取出直接處理
// for (const matched of results) {}
// 獲得結果同上

注意,matchAll()不像match()同樣返回數組,而是返回一個迭代器,對大數據量的場景更友好

for-in 遍歷機制

JavaScript 中經過for-in遍歷對象時 key 的順序是不肯定的,由於規範沒有明肯定義,而且可以遍歷原型屬性讓for-in的實現機制變得至關複雜,不一樣 JavaScript 引擎有各自根深蒂固的不一樣實現,很難統一

因此 ES2020 不要求統一屬性遍歷順序,而是對遍歷過程當中的一些特殊 Case 明肯定義了一些規則:

  • 遍歷不到 Symbol 類型的屬性
  • 遍歷過程當中,目標對象的屬性能被刪除,忽略掉還沒有遍歷到卻已經被刪掉的屬性
  • 遍歷過程當中,若是有新增屬性,不保證新的屬性能被當次遍歷處理到
  • 屬性名不會重複出現(一個屬性名最多出現一次)
  • 目標對象整條原型鏈上的屬性都能遍歷到

具體見13.7.5.15 EnumerateObjectProperties

globalThis

最後一個新特性是globalThis,用來解決瀏覽器,Node.js 等不一樣環境下,全局對象名稱不統一,獲取全局對象比較麻煩的問題:

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');
};

globalThis做爲統一的全局對象訪問方式,老是指向全局做用域中的this值:

The global variable globalThis is the new standard way of accessing the global object. It got its name from the fact that it has the same value as this in global scope.

P.S.爲何不叫global?是由於global可能會影響現有的一些代碼,因此另起一個globalThis避免衝突

至此,ES2020 的全部新特性都清楚了

六.總結

比起ES2019,ES2020 算是一波大更新了,動態 import、安全的鏈式操做、大整數支持……全都加入了豪華午飯

參考資料

有所得、有所惑,真好

關注「前端向後」微信公衆號,你將收穫一系列「用原創」的高質量技術文章,主題包括但不限於前端、Node.js以及服務端技術

本文首發於 ayqy.net ,原文連接:http://www.ayqy.net/blog/es2020/

相關文章
相關標籤/搜索