ECMAScript 雙月報告:Hashbang Grammer 提案成功進入到 Stage 4

語言: CN / TW / HK

作者 @穹心
審校 @昭朗

本次會議中,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 有以下幾個門檻:

  1. 必須編寫與所有提案內容對應的 tc39/test262 [1] 測試,用於給各大 JavaScript 引擎和 transpiler 等實現檢查與標準的相容程度,並且 test262 已經合入了提案所需要的測試用例;

  2. 至少要有兩個實現能夠相容上述 Test 262 測試,併發布到正式版本中;

  3. 發起了將提案內容合入正式標準文字 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 有以下幾個門檻:

  1. 撰寫了包含提案所有內容的標準文字,並有指定的 TC39 成員審閱並簽署了同意意見;

  2. ECMAScript 編輯簽署了同意意見。

Duplicate named capturing groups

提案連結: proposal-duplicate-named-capturing-groups [4]

在正則表示式中,我們可以使用捕獲組(Capturing Group)來對匹配模式中的某一部分做獨立的匹配,如 es+ 會匹配 essssesssss+ 代表匹配一次或更多),而使用匹配組,我們可以將 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 有以下門檻:

  1. 找到一個 TC39 成員作為 champion 負責這個提案的演進;

  2. 明確提案需要解決的問題與需求和大致的解決方案;

  3. 有問題、解決方案的例子;

  4. 對 API 形式、關鍵演算法、語義、實現風險等有討論、分析。Stage 1 的提案會有可預見的比較大的改動,以下列出的例子並不代表提案最終會是例子中的語法、語義。

Symbol Predicates

提案連結: proposal-symbol-predicates [6]

此提案為 Symbol 頂級物件引入了兩個新的方法: Symbol.isRegistered 與   Symbol.isWellKnown ,它們分別用於判斷一個 Symbol 值是否已被註冊,以及是否是 ECMA262 & ECMA402 規範中內建的 Symbol 型別(如 Symbol.iteratorSymbol.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 的問題,非常歡迎有興趣的同學參與討論:http://github.com/JSCIG/es-discuss/discussions 。

參考資料

[1]

tc39/test262: http://github.com/tc39/test262

[2]

tc39/ecma262: http://github.com/tc39/ecma262

[3]

proposal-hashbang: http://github.com/tc39/proposal-hashbang

[4]

proposal-duplicate-named-capturing-groups: http://github.com/tc39/proposal-duplicate-named-capturing-groups

[5]

proposal-import-reflection: http://github.com/tc39/proposal-import-reflection

[6]

proposal-symbol-predicates: http://github.com/rricard/proposal-symbol-predicates

[7]

proposal-policy-map-set: http://github.com/tc39/proposal-policy-map-set

[8]

lru-cache: http://www.npmjs.com/package/lru-cache

[9]

quick-lru: http://www.npmjs.com/package/quick-lru

[10]

proposal-function-memo: http://github.com/js-choi/proposal-function-memo

[11]

proposal-object-pick-or-omit: http://github.com/tc39/proposal-object-pick-or-omit