寫在前面
ES2020(即 ES11)上周(2020 年 6 月)已經正式發布,在此之前進入 Stage 4 的 10 項提案均已納入規范,成為 JavaScript 語言的新特性
一.特性一覽
ES Module 迎來了一些增強:
-
import():一種可以用動態模塊標識異步引入模塊的的語法- 由 Domenic Denicola 提出
-
import.meta:一個對象,用來攜帶模塊相關的元信息- 由 Domenic Denicola 提出
-
export * as ns from “mod”;:一種新的聚合導出語法
正式支持了安全的鏈式操作:
-
Optional chaining:新運算符
?.
能夠在屬性訪問、方法調用前檢查其是否存在 -
可選鏈 - 由 Gabriel Isenberg, Claude Pache, Dustin Savery 提出
-
Nullish coalescing Operator:用來提供默認值的新運算符
??
空值合並運算符 - 由 Gabriel Isenberg 提出
提供了大數運算的原生支持:
-
BigInt – arbitrary precision integers:一種新的基礎數值類型,支持任意精度的整數運算– 任意精度整數,由 Daniel Ehrenberg 提出
一些基礎 API 也有了新的變化:
-
Promise.allSettled:一個新的 Promise 組合器,不像
all
、race
一樣具有短路特性-由 Jason Williams, Robert Pamely 和 Mathias Bynens 提出 -
String.prototype.matchAll:以迭代器的形式返回全局匹配模式下的正則表達式匹配到的所有結果(
index
、groups
等)- 由 Jordan Harband 提出 -
globalThis:訪問全局作用域
this
的通用方法 -
for-in mechanics:規范
for-in
循環的某些行為 - 由 Kevin Gibbons 提出
二.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
聲明只能出現在頂層作用域,不支持按需加載、懶加載 -
靜態標識:模塊標識只能是字符串字面量,不支持運行時動態計算而來的模塊名
例如:
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-en
、module-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?.()
,這是為了與三目運算符中的?
區分開
機制非常簡單,如果出現在問號前的值不是undefined
或null
,才執行問號后的操作,否則返回undefined
同樣具有短路特性:
// 在 .b?.m 時短路返回了 undefined,而不會 alert 'here'
({a: 1})?.a?.b?.m?.(alert('here'))
與&&
相比,新的?.
操作符更適合安全進行鏈式操作的場景,因為:
-
語義更明確:
?.
遇到屬性/方法不存在就返回undefined
,而不像&&
一樣返回左側的值(幾乎沒什么用) -
存在性判斷更准確:
?.
只針對null
和undefined
,而&&
遇到任意假值都會返回,有時無法滿足需要
例如常用的正則提取目標串,語法描述相當簡潔:
'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
用來提供默認值,當左側的actualValue
為undefined
或null
時,返回右側的defaultValue
,否則返回左側actualValue
類似於||
,主要區別在於??
只針對null
和undefined
,而||
遇到任一假值都會返回右側的默認值
四.大數運算
新增了一種基礎類型,叫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
無法表示:
JavaScript Max Safe Integer
具體解釋見BigInts in JavaScript: A case study in TC39
BigInt
類型的出現正是為了解決此類問題:
9007199254740991n + 2n
// 9007199254740993n 正確
引入的新東西包括:
-
大整數字面量:給數字后綴一個
n
表示大整數,例如9007199254740993n
、0xFFn
(二進制、八進制、十進制、十六進制字面量通通可以后綴個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.all、Promise.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、安全的鏈式操作、大整數支持……全都加入了豪華午餐
參考資料
-
1.ECMAScript® 2020 Language Specification
-
2.https://pawelgrzybek.com/whats-new-in-ecmascript-2020/