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 的問題,非常歡迎有興趣的同學參與討論:https://github.com/JSCIG/es-discuss/discussions 。

參考資料

[1]

tc39/test262: https://github.com/tc39/test262

[2]

tc39/ecma262: https://github.com/tc39/ecma262

[3]

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

[4]

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

[5]

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

[6]

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

[7]

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

[8]

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

[9]

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

[10]

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

[11]

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