ECMAScript 雙月報告:Hashbang Grammer 提案成功進入到 Stage 4
作者 @穹心
審校 @昭朗
本次會議中,Hashbang Grammer 提案成功進入到 Stage 4,將在 ECMAScript 2023 中被作為正式語言特性加入到 JavaScript 當中。在上一次會議中獲得了階段性突破的 Duplicate named capturing groups 與 Import Reflection 提案,在本次會議中也再次實現了 Stage 的推進。除此以外,還有 Function Memoization 、Object.pick/omit 等在本次會議中首次推進到 Stage 1 的提案。
Stage 3 → Stage 4
從 Stage 3 進入到 Stage 4 有以下幾個門檻:
-
必須編寫與所有提案內容對應的 tc39/test262 [1] 測試,用於給各大 JavaScript 引擎和 transpiler 等實現檢查與標準的相容程度,並且 test262 已經合入了提案所需要的測試用例;
-
至少要有兩個實現能夠相容上述 Test 262 測試,併發布到正式版本中;
-
發起了將提案內容合入正式標準文字 tc39/ecma262 [2] 的 Pull Request,並被 ECMAScript 編輯簽署同意意見。
Hashbang Grammar
提案連結: proposal-hashbang [3]
Hashbang (也稱 Shebang)語法常用於在類 Unix 系統下指定此指令碼檔案的直譯器,它的語法大致是這樣:
#!/usr/bin/env node
console.log("ecma");
JavaScript 作為一門解釋性語言,其原始碼需要執行時將其解釋為機器碼才能執行,舉例來說,使用 node :
$ node index.js
這一命令其實就指明瞭,我們在使用 node 來解釋執行 index.js 檔案。而“使用 node”這一資訊,其實就可以通過上面的 Shebang 來將其內聯到檔案中,然後我們就可以直接執行此檔案(需要 chmod +x index.js
):
$ ./index.js
/usr/bin/env
實際上是一個可執行程式,它將基於後面的引數為我們尋找實際程式,即 /usr/bin/env node
將指向作業系統上的 node 路徑,這樣我們就不需要自己寫死 node 的安裝路徑了。
而此提案的主要作用在於,此前直譯器所獲得的 JS 程式碼是已經去除了 Shebang 的部分,而此提案會將 Shebang 的程式碼也完整地傳遞給引擎,由引擎層面來進行統一的標準化處理。
Stage 2 → Stage 3
提案從 Stage 2 進入到 Stage 3 有以下幾個門檻:
-
撰寫了包含提案所有內容的標準文字,並有指定的 TC39 成員審閱並簽署了同意意見;
-
ECMAScript 編輯簽署了同意意見。
Duplicate named capturing groups
提案連結: proposal-duplicate-named-capturing-groups [4]
在正則表示式中,我們可以使用捕獲組(Capturing Group)來對匹配模式中的某一部分做獨立的匹配,如 es+
會匹配 essss
與 esssss
( +
代表匹配一次或更多),而使用匹配組,我們可以將 es
作為一個匹配部分,如 (es)+
會匹配 es
以及 eseses
等。
我們也可以對捕獲組進行命名,如 ?<name>
這樣的形式,常見的一個場景是結合 str.match
方法:
const dateRegexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/;
const str = "2022-06-01";
const groups = str.match(dateRegexp).groups;
groups.year; // 2022
groups.month; // 06
groups.day; // 01
無法使用同名捕獲組匹配一組聯合模式,如日期格式還可能是 06-01-2022,我們希望能這麼使用聯合模式:
const dateRegexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})|(?<day>[0-9]{2})-(?<month>[0-9]{2})-(?<year>[0-9]{4})/;
但由於捕獲組的命名唯一約束,上面這個表示式是不合法的。
為了解決這一問題,此提案提出允許捕獲組的命名不唯一,以此來支援如上面在聯合模式中使用捕獲組的場景。
Stage 1 → Stage 2
從 Stage 1 進入到 Stage 2 需要完成撰寫包含提案所有內容的標準文字的初稿。
Import Reflection
提案連結: proposal-import-reflection [5]
Import Reflection 提案為 import 語句支援了在預設匯入名前新增反射型別,來宣告匯入反射屬性(元資料)的能力,其目前語法大致如下:
import module x from "<specifier>";
const x = await import("<specifier>", { reflect: "module" });
這裡的 module 即為其反射型別。這一標註會改變 import 語句的對於目標模組的執行方式,以此提案的主要驅動場景之一為例, 為 WebAssembly 模組指定額外的型別,如例項匯入( WebAssembly.Instance
)與模組匯入( WebAssembly.Module
):
import module FooModule from "./foo.wasm";
FooModule instanceof WebAssembly.Module; // true
// WASI 是適用於 WebAssembly 的模組化系統呼叫規範
import { WASI } from 'wasi';
const wasi = new WASI({ args, env, preopens });
const fooInstance = await WebAssembly.instantiate(FooModule, {
wasi_snapshot_preview1: wasi.wasiImport
});
wasi.start(fooInstance);
Stage 0 → Stage 1
從 Stage 0 進入到 Stage 1 有以下門檻:
-
找到一個 TC39 成員作為 champion 負責這個提案的演進;
-
明確提案需要解決的問題與需求和大致的解決方案;
-
有問題、解決方案的例子;
-
對 API 形式、關鍵演算法、語義、實現風險等有討論、分析。Stage 1 的提案會有可預見的比較大的改動,以下列出的例子並不代表提案最終會是例子中的語法、語義。
Symbol Predicates
提案連結: proposal-symbol-predicates [6]
此提案為 Symbol 頂級物件引入了兩個新的方法: Symbol.isRegistered
與 Symbol.isWellKnown
,它們分別用於判斷一個 Symbol 值是否已被註冊,以及是否是 ECMA262 & ECMA402 規範中內建的 Symbol 型別(如 Symbol.iterator
、 Symbol.toPrimitive
等)。
這個提案主要是為了解決在 Symbol as WeakMap Key 提案中,僅有 Unique Symbol(直接通過 Symbol()
建立的 Symbol 值) 與 Well-known Symbol(內建 Symbol) 可以作為 WeakMap 結構 key 的問題。
你也可以使用這兩個方法來判斷一個 Symbol 型別是否是獨一無二的:
const isUniqueSymbol = sym => typeof sym === "symbol" && !(Symbol.isRegistered(sym) || Symbol.isWellKnown(sym));
isUniqueSymbol(Symbol()); // true 一個新的 Symbol 型別
isUniqueSymbol(Symbol.for("foo")); // false Symbol.for 方法會將此 Symbol 註冊到全域性
isUniqueSymbol(Symbol.asyncIterator); // false 內建 Symbol 型別
isUniqueSymbol({}); // false 非 Symbol 型別
Policy Maps and Sets
提案連結: proposal-policy-map-set [7]
快取在程式設計實踐中一直是一個重要的領域,前端開發者和它打交道的次數更是數不勝數:DNS快取、HTTP快取、CDN快取、本地快取、伺服器快取等等。在 npm 社群,你也能找到許多用於快取設計的工具包,如基於 LRU 策略的 lru-cache [8] 與 quick-lru [9] 等。
此提案嘗試為 JavaScript 中引入原生的快取策略實現,包括 LRU (Least Recently Used,最近最少使用)、LFU(Least Frequently Used,最不常用)、FIFO(First In First Out,先進先出)與 LIFO (Last In First Out,後進先出),它們被實現為內建資料結構的形式:
new FIFOMap(maxNumOfEntries, entries = [])
new FIFOSet(maxNumOfValues, values = [])
new LIFOMap(maxNumOfEntries, entries = [])
new LIFOSet(maxNumOfValues, values = [])
new LRUMap(maxNumOfEntries, entries = [])
new LRUSet(maxNumOfValues, values = [])
new LFUMap(maxNumOfEntries, entries = [])
new LFUSet(maxNumOfValues, values = [])
這些結構基本實現了 Map 與 Set 上的方法(但它們並不是 Map 與 Set 的子型別),你也可以通過這些建構函式的 maxNumOfEntries / maxNumOfValues 來控制這些快取結構的可用記憶體。
Function Memoization
提案連結: proposal-function-memo [10]
函式快取指的是,對於一個函式建立起入參-結果的快取表,在函式被使用某一新的入參呼叫時的返回值快取起來,並在後續再次使用這一入參時直接返回此快取值,而不會實際呼叫函式邏輯。
對於存在較大開銷的計算過程,以及從狀態到 UI 元件的計算這種場景,函式快取會是非常好的優化手段,同時也可以基於其更好地實現單例模式(如確保對物件返回的是同一個引用)。
目前此提案提出的方式是新增 Function.prototype.memo
方法,也就是說對一個函式呼叫 memo 方法後,將返回它的快取版本:
function f (x) { console.log(x); return x * 2; }
const fMemo = f.memo();
fMemo(3); // 列印 3,返回 6
fMemo(3); // 直接返回 6
fMemo(2); // 列印 2,返回 4
fMemo(2); // 直接返回 4
fMemo(3); // 直接返回 6
為了更簡單地獲取函式的快取版本,此提案提出同時新增 @Function.memo
裝飾器,來直接將一個函式標記為快取版本(將無法再訪問原版本):
@Function.memo
function f (x) { console.log(x); return x * 2; }
另外,此提案也希望將快取表的控制也暴露出去,也就是說你可以自己傳入一個實現了 .get()
.has()
.set()
.get()
方法的類 Map 結構,來作為函式的快取控制,上面提到的 Policy Maps and Sets 提案在這裡就大有可為。
Object pick/omit
提案連結: proposal-object-pick-or-omit [11]
此提案將引入兩個 Object 物件上的頂級方法:Object.pick 與 Object.omit,它們的作用正如其名,pick 將提取物件中的特定部分,而 omit 將移除物件中的特定部分。如果你使用過 Lodash 的 pick 和 omit 方法,那麼應該對這兩種操作非常熟悉。
目前在 JavaScript 中,我們可以通過解構賦值的方式來實現類 omit 的操作:
// 移除 obj 的 name、age 屬性後得到 rest
const { name, age, ...rest } = obj;
但問題在於,如果我們想要移除的鍵名是動態的,那麼這一方式就完全失效了,同時也無法基於解構賦值實現類 pick 的操作(pick 應當是基於子集進行處理,而非反過來基於差集)。另外,解構賦值並不能對原型物件上的屬性進行處理。
使用這兩個方法,我們可以進行更加符合直覺的物件操作了:
Object.pick(obj, ['job', 'sex']);
Object.omit(obj, ['name', 'age']);
除了基於鍵名來進行操作,這兩個方法也支援使用一個 predictedFunction 函式來進行基於鍵值的判斷,在此條件中返回 true 的屬性將對應的被保留/移除:
Object.pick({a : 1, b : 2}, v => v === 1); // => { a: 1 }
這一使用方式類似於 Lodash 中的 pickBy / omitBy 方法。
結語
由賀師俊牽頭,阿里巴巴前端標準化小組等多方參與組建的 JavaScript 中文興趣小組(JSCIG,JavaScript Chinese Interest Group)在 GitHub 上開放討論各種 ECMAScript 的問題,非常歡迎有興趣的同學參與討論:https://github.com/JSCIG/es-discuss/discussions 。
參考資料
tc39/test262: https://github.com/tc39/test262
tc39/ecma262: https://github.com/tc39/ecma262
proposal-hashbang: https://github.com/tc39/proposal-hashbang
proposal-duplicate-named-capturing-groups: https://github.com/tc39/proposal-duplicate-named-capturing-groups
proposal-import-reflection: https://github.com/tc39/proposal-import-reflection
proposal-symbol-predicates: https://github.com/rricard/proposal-symbol-predicates
proposal-policy-map-set: https://github.com/tc39/proposal-policy-map-set
lru-cache: https://www.npmjs.com/package/lru-cache
quick-lru: https://www.npmjs.com/package/quick-lru
proposal-function-memo: https://github.com/js-choi/proposal-function-memo
proposal-object-pick-or-omit: https://github.com/tc39/proposal-object-pick-or-omit
- 為iframe正名,你可能並不需要微前端
- SLS:基於 OTel 的移動端全鏈路 Trace 建設思考和實踐
- 為iframe正名,你可能並不需要微前端
- 釘釘 ANR 治理最佳實踐 | 定位 ANR 不再霧裡看花
- 釘釘 ANR 治理最佳實踐 | 定位 ANR 不再霧裡看花
- 低程式碼多分支協同開發的建設與實踐
- Flutter for Web 首次首屏優化——JS 分片優化
- 全新的 React 元件設計理念 Headless UI
- 我們是如何追逐元宇宙、XR等“概念股”浪潮的?
- 盒馬 iOS Live Activity &“靈動島”配送場景實踐
- ECMAScript 雙月報告:Hashbang Grammer 提案成功進入到 Stage 4
- 如何根治 Script Error.
- 語雀桌面端技術架構實踐
- 語雀桌面端技術架構實踐
- Clang Module 內部實現原理及原始碼分析
- 基於 LowCodeEngine 的除錯能力建設與實踐
- 基於 LowCodeEngine 的除錯能力建設與實踐
- Android Target 31 升級全攻略 —— 記阿里首個超級 App 的坎坷升級之路