ECMAScript 最新進展匯總!
2023 年 7 月 11 日 - 7 月 13 日,第 97 次 TC39 會議在挪威舉行,下面就來看看在這次會議中哪些 ECMAScript 提案取得了新進展吧!
TC39 是一個推動 JavaScript 發展的技術委員會,由各個主流瀏覽器廠商的代表構成,其主要工作就是制定 ECMAScript 標準。TC39 每兩個月舉行一次會議。對于新提案,從提出到最后被納入 ECMAScript 新特性,TC39 的規范中分為五步:
- Stage 0(strawman),任何TC39的成員都可以提交。
- Stage 1(proposal),進入此階段就意味著這一提案被認為是正式的了,需要對此提案的場景與API進行詳盡的描述。
- Stage 2(draft),這一階段的提案如果能最終進入到標準,那么在之后的階段都不會有太大的變化,因為理論上只接受增量修改。
- Stage 3(candidate),這一階段的提案只有在遇到了重大問題才會修改,規范文檔需要被全面的完成。
- Stage 4(finished),這一階段的提案將會被納入到ES每年發布的規范之中。
附: ECMAScript 2023(ES14)已于 6 月 27 日正式發布,詳見 >>> 《ECMAScript 2023 正式發布,有哪些新特性?》
Stage 3
數組分組
該 提案[1] 用于簡化數組(和可迭代對象)中的分組操作。數組分組是一種非常常見的操作,其將相似的數據組合成組允許開發者計算更高階的數據集。
const array = [1, 2, 3, 4, 5];
// Object.groupBy 根據任意鍵對元素進行分組,這里通過奇偶數對元素進行分組。
Object.groupBy(array, (num, index) => {
return num % 2 === 0 ? 'even': 'odd';
});
// => { odd: [1, 3, 5], even: [2, 4] }
// Map.groupBy 返回一個 Map 對象,方便使用對象鍵進行分組。
const odd = { odd: true };
const even = { even: true };
Map.groupBy(array, (num, index) => {
return num % 2 === 0 ? even: odd;
});
// => Map { {odd: true}: [1, 3, 5], {even: true}: [2, 4] }
該提案提供了兩個方法:Object.groupBy 和 Map.groupBy。前者返回一個沒有原型的對象,可以方便地進行解構操作,并且可以防止與全局 Object 屬性發生意外沖突。后者返回一個普通的 Map 實例,可以對復雜鍵類型進行分組(比如復合鍵或元組)。
Promise.withResolvers
當手動創建一個 Promise 時,用戶必須傳遞一個執行器回調函數,該函數接受兩個參數:
- resolve 函數,用于觸發 Promise 的解決。
- reject 函數,用于觸發 Promise 的拒絕。
如果回調函數可以嵌入調用一個最終觸發解決或拒絕的異步函數(例如注冊事件監聽器),則這種方式可以很好地工作。
const promise = new Promise((resolve, reject) => {
asyncRequest(config, response => {
const buffer = [];
response.on('data', data => buffer.push(data));
response.on('end', () => resolve(buffer));
response.on('error', reason => reject(reason));
});
});
然而,通常開發人員希望在實例化 Promise 后配置其解決和拒絕行為。目前,這需要一個繁瑣的解決方法,從回調范圍中提取 resolve 和 reject 函數:
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
asyncRequest(config, response => {
const buffer = [];
response.on('callback-request', id => {
promise.then(data => callback(id, data));
});
response.on('data', data => buffer.push(data));
response.on('end', () => resolve(buffer));
response.on('error', reason => reject(reason));
});
開發人員可能還有其他要求,需要將 resolve/reject 傳遞給多個調用方,因此必須以這種方式實現:
let resolve = () => { };
let reject = () => { };
function request(type, message) {
if (socket) {
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
socket.emit(type, message);
return promise;
}
return Promise.reject(new Error('Socket unavailable'));
}
socket.on('response', response => {
if (response.status === 200) {
resolve(response);
}
else {
reject(new Error(response));
}
});
socket.on('error', err => {
reject(err);
});
Promise.withResolvers[2] 提案簡單地在 Promise 構造函數中添加了一個靜態方法,暫時稱為 withResolvers
,該方法返回一個 Promise,并方便地公開其解決和拒絕函數。
const { promise, resolve, reject } = Promise.withResolvers();
源階段導入
無論是對于 JavaScript 還是 WebAssembly,都需要能夠更緊密地定制模塊的加載、鏈接和執行,超出了標準的宿主執行模型。
- 對于 JavaScript,創建自定義加載器需要一種模塊源類型,以便共享宿主的解析、執行、安全性和緩存語義。
- 對于 WebAssembly,WebAssembly 模塊的導入和導出通常需要進行自定義的檢查和封裝,以便正確設置,這通常需要手動的獲取和實例化工作,在當前的宿主 ESM 集成提案中沒有提供相應支持。
通過將語法模塊源導入支持作為新的導入階段,可以創建一個基礎機制,將模塊的靜態、安全性和工具化優勢從 ESM 集成擴展到這些動態實例化用例。
該提案[3]允許ES模塊從主機提供的編譯后的模塊源的反映表達式進行導入:
import source x from "<specifier>";
僅支持上述形式的導入,不支持命名導出和未綁定聲明。
動態形式使用 import.<phase>:
const x = await import.source("<specifier>");
通過將階段作為顯式語法的一部分,可以在靜態上下文中靜態區分全動態導入和僅用于源的導入(無需處理依賴項)。
處理時間區域規范化的變化
ECMAScript中的時間區域依賴于IANA時區數據庫(TZDB)的標識符,如America/Los_Angeles或Asia/Tokyo。該提案旨在改善開發人員在TZDB中更改時間區域的規范標識符(例如從Europe/Kiev到Europe/Kyiv)時的開發體驗。
減少實現之間以及實現與規范之間的差異
- 已完成 - 簡化處理時區標識符的抽象操作。
- 已完成 - 澄清規范以防止更多的分歧。
- 在 Temporal 廣泛采用之前,幫助V8和WebKit更新13個過時的規范標識(如Asia/Calcutta,Europe/Kiev和Asia/Saigon),以免出現問題。
- 制定規范文本以減少實現之間的分歧。這一步需要在實現者和TG2(ECMA-402團隊)之間找到共同點,討論規范化應該如何工作。
減少標準化變化的影響
- 避免對鏈接進行可觀察的跟隨。如果標準化變化不會影響現有代碼,那么未來的標準化變化就不太可能破壞Web。由于標準化是實現定義的,這個變化(或許會、也許不會;需要進一步研究)在Temporal第4階段之后發布可能是安全的,但最好不要等太久。
Temporal.TimeZone.from('Asia/Calcutta');
// => Asia/Kolkata(Firefox上當前的Temporal行為)
// => Asia/Calcutta(建議:在將標識符返回給調用方時,不要遵循鏈接)
- 添加Temporal.TimeZone.prototype.equals方法。由于(5)會在創建TimeZone對象時停止標準化標識符,因此有一個直觀的方法來判斷兩個 TimeZone 對象是否表示相同的時區。
// 更人性化的標準化相等性測試
Temporal.TimeZone.from('Asia/Calcutta').equals('Asia/Kolkata');
// => true
Stage 2
Time Zone Canonicalization[4]
JavaScript應用程序可能會變得非常龐大,以至于即使加載它們的初始化腳本,執行起來也會產生顯著的性能開銷。通常,這種情況發生在應用程序的生命周期較晚的階段,往往需要進行大規模的改動以提高性能。加載性能是一個重要的改進領域,涉及預加載技術以避免瀑布效應,并使用動態導入進行模塊的惰性加載。
盡管使用了這些技術解決了加載性能問題,但代碼本身的編寫方式仍會導致執行性能開銷和CPU瓶頸在初始化過程中出現。
該提案[5]是引入一種新的導入語法形式,它將始終返回一個命名空間對象。在使用時,模塊及其依賴項不會被執行,但會完全加載到可以執行的狀態,然后才會認為模塊圖已加載完成。只有當訪問該模塊的屬性時,才會執行相應的操作。
該API將使用以下語法:
// 或使用自定義關鍵字:
import defer * as yNamespace from "y";
Stage 1
DataView get/set Uint8Clamped 方法
現在只有其中 10 個具有DataView的 get/set 方法。
該提案[6]旨在添加DataView.prototype.getUint8Clamped和DataView.prototype.setUint8Clamped方法。
- getUint8Clamped(offset: number): number:從指定的偏移量讀取一個8位無符號整數(Uint8Clamped)值,并返回該值。
- setUint8Clamped(offset: number, value: number): void:將一個8位無符號整數(Uint8Clamped)值寫入到指定的偏移量。
可選鏈賦值
該提案[7]建議在賦值運算符左側添加對可選鏈的支持:a?.b = c。在實際開發中,經常需要對對象的屬性進行賦值,但前提是該對象確實存在。
通常的做法是使用if語句來保護賦值操作:
if (obj) {
obj.prop = value;
}
新語法和現有語法對比如下:
相關鏈接
[1]提案: https://github.com/tc39/proposal-array-grouping。
[2]Promise.withResolvers: https://github.com/tc39/proposal-promise-with-resolvers。
[3]提案: https://github.com/tc39/proposal-source-phase-imports。
[4]Time Zone Canonicalization: https://github.com/tc39/proposal-canonical-tz。
[5]提案: https://github.com/tc39/proposal-defer-import-eval。
[6]提案: https://github.com/tc39/proposal-dataview-get-set-uint8clamped。
[7]提案: https://github.com/tc39/proposal-optional-chaining-assignment。