作者 | 昭朗
阿裡巴巴目前是 ECMA 的合作會員,阿裡經濟體前端委員會會有代表參加相關會議讨論,我們會持續為大家分享 TC39 核心會議上關于 ECMAScript 的最新進展,歡迎大家關注。
Stage 3 -> Stage 4
從 Stage 3 進入到 Stage 4 有以下幾個門檻:
- 必須編寫與所有提案内容對應的 tc39/test262 測試,用于給各大 JavaScript 引擎和 transpiler 等實作檢查與标準的相容程度,并且 test262 已經合入了提案所需要的測試用例;
- 至少要有兩個實作能夠相容上述 Test 262 測試,并釋出到正式版本中;
- 發起了将提案内容合入正式标準文本 tc39/ecma262 的 Pull Request,并被 ECMAScript 編輯簽署同意意見。
Promise.any & AggregateError
提案連結目前 Promise 主要的組合操作會有:

一個簡單的 Promise.any 的使用例子:
Promise.any([
fetch('https://example.com/').then(() => 'home'),
fetch('https://example.com/blog').then(() => 'blog'),
fetch('https://example.com/docs').then(() => 'docs')
]).then((first) => {
// 任一 Promise 被 resolve.
console.log(first);
// → 'home'
}).catch((error) => {
// 當上述所有的 Promise 都被 reject.
console.log(error);
});
從例子中我們也可以看到,Promise.any 隻有在所有的輸入的 Promise 都被 reject 後才會被 reject,那麼它的 catch 所擷取到的參數會如何表示 reject Promise.any 的所有異常資訊呢?
這即是這個提案另一個部分 AggregateError。AggregateError 由一系列 Error 組成,可以通過 AggregateError.errors 屬性擷取這些 Error 執行個體。
WeakRefs & FinalizationRegistry
在 ES6 中引入的 WeakMap 和 WeakSet 也可以來實作的某種程度的弱引用,不過這個弱引用不是真的弱引用,本質上其實是 Ephemeron 表。在 Ephemeron 的實作中,key 是對象,隻要 key 一直還在那麼 value 就不會釋放。而 WeakRef 則沒有這個問題,通過 WeakRef 生成的引用,存儲在普通的 Map 中時也可以正常釋放。
在此之前,JavaScript 裡引用釋放沒有回調觸發。新的 WeakRef 提案裡有類似析構函數的 FinalizationRegistry 支援。FinalizationRegistry 與析構機制的差別在于,析構函數能夠拿到目前對象,而 FinalizationRegistry 中注冊的回調并不能拿到被釋放(或即将被釋放)的對象,而是一個在往 FinalizationRegistry 中注冊這個對象時,一同傳入的 heldValue 參數。通過這個 heldValue 我們能夠獲知是哪一個對象被釋放并觸發了這一次的終結回調。
const cache = new Map();
const finalizationGroup = new FinalizationRegistry((name) => {
const ref = cache.get(name);
if (ref !== undefined && ref.deref() === undefined) {
cache.delete(name);
}
});
function getImageCached(name) {
const ref = cache.get(name); // 1
if (ref !== undefined) { // 2
const deref = ref.deref();
if (deref !== undefined) return deref;
}
const image = performExpensiveOperation(name); // 3
const wr = new WeakRef(image); // 4
cache.set(name, wr); // 5
finalizationGroup.register(image, name); // 6
return image; // 7
}
Logical Assignment
這個提案定義了邏輯操作并指派表達式,包含新增的三個邏輯操作并指派操作符,邏輯或并指派 ||,邏輯與并複制 &&,空值指派 ??=:
// "Or Or Equals"
a ||= b;
a || (a = b);
// "And And Equals"
a &&= b;
a && (a = b);
// "QQ Equals"
a ??= b;
a ?? (a = b);
function example(opts) {
opts.foo = opts.foo ?? 'bar';
opts.baz ??= 'qux';
}
NumericLiteralSeparator
非常長的數字字面量都非常難以閱讀,特别是有非常多重複的數字,甚至需要借助外部工具來輔助閱讀📏。
1000000000 // 十萬?百萬?千萬?
const FEE = 12300;
// 應該讀成 12,300 嗎? 或者因為它的機關是分(人民币),是以應該讀成 123 元?
這個提案讓我們可以使用 _ (U+005F) 作為長數字字面量的分隔符,讓數字字面量更加易讀。
10_0000_0000 // 是以這是十億
let fee = 123_00; // ¥123元 (12300 分)
let amount = 123_4500; // 123萬4500 (中文閱讀習慣)
let amount = 1_234_500; // 1,234,500 (英文書寫習慣)
// 同樣可以用在小數部分與指數部分
0.000_001 // 百萬分之一
1e10_000 // 10^(10 000)
Intl.ListFormat
這是一個 ECMA 402 國際化提案。這個提案提供了基于語言的清單格式化。而清單格式化也是一個常見的本地化需求:
let lfmt = new Intl.ListFormat("zh", {
type: "conjunction", // "conjunction", "disjunction" or "unit"
style: "long", // "long", "short" or "narrow"
});
console.log(lfmt.format(["Anne", "John", "Mike"])); // "Anne、John和Mike"
Intl.DateTimeFormat dateStyle/timeStyle
這個提案也是一個 ECMA 402 國際化提案。不同的語言、本地化偏好對于不同長度的日期時間格式化時,都有不同的偏好:
- en-US 格式短日期 7/27/20,相當于選項 year 為 2-digit,month 為 short,day 為 numeric;
- zh-CN 格式短日期 2020/7/27,相當于選項 year 為 numeric,month 為 numeric,day 為 numeric;
通過内置的标準化 dateStyle 和 timeStyle 選項,提供更加符合本地化配置的格式化,而不需要開發者自行根據本地化選項選擇合适的格式化選項。
let dtf = new Intl.DateTimeFormat("zh", {
year: "numeric", // "numeric", "2-digit"
month: "numeric", // "short", "numeric", "2-digit"
day: "numeric", // "numeric", "2-digit"
});
console.log(dtf.format(new Date())); // "2020年7月27日"
// 通過新的 dateStyle/timeStyle 選項:
let dtf = new Intl.DateTimeFormat("zh", {
dateStyle: "short", // "full", "long", "medium" or "short"
});
console.log(dtf.format(new Date())); // "2020/7/27"
Stage 2 -> Stage 3
從 Stage 2 進入到 Stage 3 有以下幾個門檻:1. 撰寫了包含提案所有内容的标準文本,并有指定的 TC39 成員審閱并簽署了同意意見;2. ECMAScript 編輯簽署了同意意見。
iterator.items()
很多時候,類似于 Python 中的數組負值索引可以非常實用。比如在 Python 中我們可以通過 arr[-1] 來通路數組中的最後一個元素,而不用通過目前 JavaScript 中的方式來通路 arr[arr.length-1]。這裡的負數是作為從起始元素(即arr[0])開始的反向索引。
但是現在 JavaScript 中的問題是,[] 這個文法不僅僅隻是在數組中使用(當然在 Python 中也不是),而在數組中也不僅僅隻可以作為索引使用。像arr[1]一樣通過索引引用一個值,事實上引用的是這個對象的 "1" 這個屬性。是以 arr[-1] 已經完全可以在現在的 JavaScript 引擎中使用,隻是它可能不是代表的我們想要表達的意思而已:它引用的是目标對象的 "-1"這個屬性,而不是一個反向索引。
這個場景其實也不是第一次在TC39提出了,比如現在已經是 Stage 1 的 Array.prototype.lastItem 提案,而與 Array.prototype.lastItem 提案相比,這次這個提案提供了一個更加通用的方案,我們可以通過任意可索引的類型(Array,String,和 TypedArray)上的 .item 方法,來通路任意一個反向索引、或者是正向索引的元素。
Intl.Segmenter
很多語言都有詞分割與句分割。Unicode UAX 29 定義了文本元素的分割算法,可以在文本中找出不同文本元素的分界線(包括如中文,韓文,日文,泰文等基于詞典分割的東亞語言)。這對于實作更加可靠的輸入法、文本編輯器、文本處理都有非常大的幫助。
在 Unicode UAX 29 中定義的文本元素、詞句分割算法的實作如果在浏覽器、JavaScript 中原生實作的話,相比于開發者們引入自己的實作方案來說,可以節省非常多的帶寬與記憶體。
let segmenter = new Intl.Segmenter("zh", {granularity: "word"});
// Use it to get an iterator for a string
let input = "我不是,我沒有,你别瞎說。";
let segments = segmenter.segment(input);
// Use that for segmentation!
for (let {segment, index, isWordLike} of segments) {
console.log("segment at code units [%d, %d): «%s»%s",
index, index + segment.length,
segment,
isWordLike ? " (word-like)" : ""
);
}
// console.log output:
// segment at code units [0, 3): «我不是» (word-like)
// segment at code units [3, 4): «,»
// segment at code units [4, 5): «我» (word-like)
// segment at code units [5, 7): «沒有» (word-like)
// segment at code units [7, 8): «,»
// segment at code units [8, 9): «你» (word-like)
// segment at code units [9, 10): «别» (word-like)
// segment at code units [10, 12): «瞎» (word-like)
// segment at code units [11, 12): «說» (word-like)
// segment at code units [12, 13): «。»
Stage 1 -> Stage 2
從 Stage 1 進入到 Stage 2 需要完成撰寫包含提案所有内容的标準文本的初稿。
WeakRef CleanupSome
WeakRef 與 FinalizationRegistry 允許 JavaScript 應用去觀測垃圾回收的過程。這個觀測性給實作帶來了非常多潛在的問題,目前提案通過将可觀測的時間點交由宿主環境如浏覽器來決定,如什麼時候 FinalizationRegistry 的回調會被調用等來規避這些問題。通常,宿主環境,如浏覽器,都會通過 microtask queue 等基于循環隊列的方式來實作這個方案:WeakRefs 從“可以觀測到值”到無法觀測到值“的變化會發生在每一個 microtask 檢查點上(即所有的 Promise 任務執行完成);類似的,FinalizationRegistry 的 cleanup 回調也會在所有的 Promise 任務執行完成後被調用。是以,隻有當我們把程式的執行權交還給事件隊列後,WeakRef 與 FinalizationRegistry 才能按預期的行為工作。
而當場景結合上 Web Workers,SharedArrayBuffer,WebAssembly 等共享記憶體的場景,我們可能在一次執行中做非常多的計算工作、與其他執行環境通過共享記憶體與原子量互動的工作,這個過程中也沒有将執行權交還給事件隊列(或者很少将執行權交還給事件隊列)。而 WebAssembly 的場景就非常比對這個特殊情況。那麼能不能有除了間斷性地停止代碼執行,交出執行權以外,其他的方法來讓 WeakRef 與 FinalizationRegistry 工作呢?
這即是這個提案所期望解決的問題。FinalizationRegistry.prototype.cleanupSome接受一個函數作為參數(後續提案更新可能會直接使用 FinalizationRegistry 的 cleanup 回調),然後 JavaScript 實作可能會在後續調用這個回調,就如同 FinalizationRegistry 的 cleanup 回調一樣,但是是以一個同步的方式完成。
值得注意的是,這個提案是從 WeakRefs 提案(Stage 3)中直接分離出來的提案,沒有經過 Stage 1 的流程,而是直接作為 Stage 2 提案發起 Review。
Record and Tuple
Record 與 Tuple 與通過 Object.free 當機的對象與使用者代碼中的 class 對象的主要差別是 Record 與 Tuple 是原始類型。另外,Object 與 Array 的相等性取決于他們是否是同一個執行個體({} !== {}),而 Record 與 Tuple 是否相等取決于他們的值(#[0, 0] === #[0, 0])。除此之外,Record 與 Tuple 都可以使用與普通對象與數組一樣的方式通路字段與元素,如((#[0, 0])[0] === 0)。
Record 與 Tuple 作為組合原始類型,同樣可以作為原始類型在 JavaScript 中的各種場景使用:
const grid = new Map([
[#[0, 0], "player"],
[[0,0], "player"],
[#{x:3, y:5}, "enemy"],
[{x:3, y:5}, "enemy"],
]);
console.log(grid.get(#[0, 0])); // player
console.log(grid.get([0,0])); // undefined
console.log(grid.get(#{x:3, y:5})); // enemy
console.log(grid.get({x:3, y:5})); // undefined
JSON.parse source text access
JSON.parse is lossy, even with a reviver function. Replacer function output is subject to re-serialization
JSON.parse 無法對超出 IEEE 754 精度的數字精準解析—即使通過現在的 reviver 函數參數也不行。類似的,在 JSON.stringify 中,我們也無法通過 replacer 函數參數序列化 JSON 中不存在的類型。
// Numbers are subject to IEEE 754 precision limits.
JSON.parse(" 9999999999999999")
// → 10000000000000000
// …and reviver functions receive already-parsed values.
JSON.parse(" 9999999999999999", (key, val) => BigInt(val))
// → 10000000000000000n
// Strings get quoted
JSON.stringify(9999999999999999n, (key, val) => String(val))
// → "\"9999999999999999\""
// Unserializeable types get rejected
JSON.stringify(9999999999999999n, (key, val) => val)
// → TypeError
這個提案提出給 reviver 函數與 replacer 函數分别增加一個新的參數,原始 source 文本與一個 rawTag Symbol。其中,對于 replacer 函數的修改是這次會議新提出的内容,目前還沒有非常詳細的使用場景例子與 spec 文本,需要後續繼續跟蹤對于 replacer 的修改如何解決序列化的問題。
// Numbers are still subject to IEEE 754 precision limits.
JSON.parse(" 9999999999999999")
// → 10000000000000000
// …but reviver functions gain access to the raw source.
JSON.parse(" 9999999999999999", (key, val, {source}) => BigInt(source))
// → 9999999999999999n
// Emit a literal sequence of BigInt digits
JSON.stringify(9999999999999999n, (key, val, {rawTag}) =>
({[rawTag]: String(val)}) )
// → "9999999999999999"
Stage 0 -> Stage 1
從 Stage 0 進入到 Stage 1 有以下門檻:1. 找到一個 TC39 成員作為 champion 負責這個提案的演進;2. 明确提案需要解決的問題與需求和大緻的解決方案;3. 例子;4. 對 API 形式、關鍵算法、語義、實作風險等有讨論、分析。
Stage 1 的提案會有可預見的比較大的改動,以下列出的例子并不代表提案最終會是例子中的文法、語義。
await operations
目前,await 關鍵字對于一個全是 Promise 數組來說無法達成類似 co 的語義:
// co
const co = require('co');
co(function* () {
var result = yield Promise.all([ Promise.resolve(true) ]);
console.log(result) // => [ true ];
var result = yield [ Promise.resolve(true) ];
console.log(result) // => [ true ];
});
// async/await
(async function () {
var result = await Promise.all([ Promise.resolve(true) ]);
console.log(result) // => [ true ];
var result = await [ Promise.resolve(true) ];
console.log(result) // => [ Promise {true} ];
})();
暫且不論 co 激進的語義能否讓開發者更好地了解、産生語義二義性。但是至少如果 await *如果同時具有 Promise.all 的語義,那我們就無法給 await 增加其他常見的 Promise 組合操作語義了,如 race,allSettled,any 等。
是以目前提案提出了如 await.all 等文法糖,讓常見的 Promise 組合操作更加便于書寫、便于閱讀:
// before
await Promise.all(users.map(async x => fetchProfile(x.id)))
// after
await.all users.map(async x => fetchProfile(x.id))
Array.prototype.unique()
去重在資料進行中是一個非常非常常見的操作。我們可以通過 [...new Set(array)] 來比較友善地去重,但是這個方式隻适用于原始量,如數字、字元串等,而無法對一系列複雜結構對象進行去重。
提案提出了一個可以接受一個函數作為擷取去重标記的 Array.prototype.unique 方法,可以用于複雜結構對象的場景:
arr.unique()
// eq [...new Set(arr)] or …?
arr.unique(x => x.foo.bar.name)
// deduplicate by the provided function
ResizableArrayBuffer and GrowableSharedArrayBuffer
這個提案主要是期望給 WebAssembly 的場景提供更加方面的記憶體擴充方式。目前調整一個 ArrayBuffer 的大小需要複制内容,但是複制非常慢,而且可能導緻記憶體空間碎片化。
提案提出了兩種新的 ArrayBuffer 類型:ResizableArrayBuffer 和 GrowableSharedArrayBuffer。
ResizableArrayBuffer 是一個内部存儲區域可以拆卸的 ArrayBuffer。設計上希望 ResizableArrayBuffer 可以原地調整大小,但是提案沒有對調整大小是否能夠觀測做要求(改變位址等)。同樣,提案目前也沒有對調整大小的方案有做描述。
let rab = new ResizableArrayBuffer(1024, 1024 ** 2);
assert(rab.byteLength === 1024);
assert(rab.maximumByteLength === 1024 ** 2);
rab.resize(rab.byteLength * 2);
assert(rab.byteLength === 1024 * 2);
GrowableSharedArrayBuffer 是可以在多個執行環境中共享的 ArrayBuffer,但是考慮到多個執行環境的同步,是以 GrowableSharedArrayBuffer 是一個隻能增長而不能縮減大小的設計。
另外,提案目前對于可調整大小的 ResizableArrayBuffer 和 GrowableSharedArrayBuffer 的最大增長大小的邊際條件既沒有定義也沒有最佳使用場景,會在接下來、和進入 Stage 2 後撰寫 spec 文本的過程中,繼續探索這些情況。
關注「Alibaba F2E」
把握阿裡巴巴前端新動向