作者 | 吳成忠(昭朗)
這次 TC39 會議是 2020 年度最後一次全員會議。這次會議中沒有任何提案争取到從 Stage 2 進入 Stage 3 的共識,也沒有提案從 Stage 3 進入 Stage 4。
Stage 1 → Stage 2
從 Stage 1 進入到 Stage 2 需要完成撰寫包含提案所有内容的标準文本的初稿。
Error Cause
提案連結:
https://github.com/tc39/proposal-error-cause提案文本連結:
https://tc39.es/proposal-error-cause/這個提案為 Error Constructor 新增了一個可選的參數
cause
,可以接受任意 JavaScript 值(JavaScript 可以 throw 任意值),并會把這個值指派到
cause
屬性上。
錯誤原因的特性在許多其他語言中都有類似的設計,如 C# Exception Cause,Java Exception Cause,Python raise exception from cause。同樣的,在龐大的 JavaScript 生态中也已經有了非常廣泛的使用,如 verror 每周有上千萬的下載下傳量,@netflix/nerror 每周有數十萬的下載下傳量。
不過,即使在 JavaScript 第三方庫中有再多的使用,Chrome DevTools 等開發者工具也難以依賴這些第三方庫中定義的、不是語言定義中存在的屬性。有了這個提案之後,這些開發者工具也可以預設列印
cause
屬性中的值了,可以為異常處理帶來更良好的體驗。
try {
return await fetch('//unintelligible-url-a')
.catch(err => {
throw new Error('Download raw resource failed', err)
})
} catch (err) {
console.log(err)
console.log('Caused by', err.cause)
// Error: Upload job result failed
// Caused by TypeError: Failed to fetch
}
Stage 0 → Stage 1
從 Stage 0 進入到 Stage 1 有以下門檻:
- 找到一個 TC39 成員作為 champion 負責這個提案的演進;
- 明确提案需要解決的問題與需求和大緻的解決方案;
- 有問題、解決方案的例子;
- 對 API 形式、關鍵算法、語義、實作風險等有讨論、分析。
Stage 1 的提案會有可預見的比較大的改動,以下列出的例子并不代表提案最終會是例子中的文法、語義。
JavaScript Module Blocks
https://github.com/tc39/proposal-js-module-blocks目前很多裝置都有非常多的處理器核心,JavaScript 代碼在多個執行單元(線程、程序,如 Web Worker,Node.js 程序,Node.js
worker_threads
)中同時執行也是越來越常見。但是現在的 JavaScript 對于多個執行單元間共享 JavaScript 代碼并沒有非常理想的方案,通常我們可以
1、将不同執行單元間的代碼寫在不同的檔案裡:
const Worker = new Worker('./my-worker.js')
worker.postMessage({ action: 'add', params: [40, 2] })
worker.addEventListener('message', data => alert(`Result: ${data}`))
2、通過一個通用的 Worker 執行器,然後将期望執行的 JavaScript 直接以字元串形式發送過去并執行(即每一次執行 JavaScript 引擎都需要重新解析這段 JavaScript 代碼):
const result = await runInWorker('return "foobar"')
3、通過一個通用的 Worker 執行器,接受一個函數并将這個函數 toString 後直接以字元串發送執行:
function add(lhs, rhs) {
// 這裡捕獲了外部變量的話,難以檢測
return lhs + rhs;
}
const result = await runInWorker(add, 40, 2)
// 不支援 async function
async function generateGraph() {
/** complex code */
}
這些方式要麼不夠工效,要麼效率較差、更有安全風險。而這個提案則提出了一個隔離了變量作用域的代碼塊,并且這個代碼塊中的代碼可以實作解析一次,到處使用。也意味着代碼塊中有任何文法問題,都可以快速地在引擎第一次解析即可被檢查出來:
let workerCode = module {
onmessage = function({ data }) {
let mod = await import(data);
postMessage(mod.fn());
};
};
let worker = new Worker(workerCode, { type: 'module' });
worker.onmessage = ({ data }) => alert(data);
worker.postMessage(module { export function fn() { return 'hello!' } });
其實在之前 TC39 已經有過類似的提案 Blöck。不過之前這個提案的主要推進者已經退出了 TC39,提案也長期未有更新,故現在有新的委員會代表繼續拾起想法開始推進。
Extensions
https://github.com/hax/proposal-extensions在這個提案之前,TC39 已經有過多個相關的提案,如 Pipeline Operator (Stage 1) ,Bind Operator (Stage 0)。其中 Bind Operator 的主要 Champion Group 成員已經長期沒有推動提案的演進,而這次這個 Extensions 提案即是重新拾起了 Bind Operator 的大部分想法,并作了部分設計上的修訂,繼續提案的推進。
目前提案在技術委員會上的示範内容包含了幾個部分的動機:
- 為任意 JavaScript 值引入局部作用域的方法與通路器(Accessor)擴充,替代 Monkey Patch 成為 JavaScript 中具有高工效、隔離性的擴充機制;
- 為擴充聲明定義一個獨立的命名空間,避免擴充聲明被普通變量聲明覆寫。
提案的内容可以簡單地總結為以下幾個例子:
// 為集合類型拓展 toSet 的方法定義
const ::toSet = function () { return new Set(this) }
// 給 DOM 對象拓展 allDivs 屬性通路器
const ::allDivs = function {
get() { return this.querySelectorAll('div') }
}
const ::flatMap = Array.prototype.flatMap
const ::size = Object.getOwnPropertyDescriptor(Set.prototype, 'size')
let classCount = document
::allDivs
::flatMap(element => element.classList)
::toSet()
::size
除了直接在局部作用域聲明擴充之外,還可以從外部子產品導入擴充聲明,或者是使用
namespaceOrConstructor:method
來使用快捷通路一個擴充:
import ::{ identity } from '某些子產品'
arrayLike
::Array:map(x => x) // Array.prototype.map.call(arrayLike, x => x)
::identity() // identity.call(arrayLike)
相對于之前的 Bind Operator 提案來說,目前的 Extensions 提案有以下不同之處:
- 增加擴充字段通路器的定義;
- 對于局部定義的擴充方法、字段通路器有獨立的變量命名空間;
- 增加使用命名空間擴充(obj::namespace:extension)的文法;
- :: 操作符現在與 . 操作符的優先級相同;
- 移除了 ::obj.foo (BindExpression),這個表達式相當于是 foo.bind(obj) 的文法糖。
相比于 Pipeline Operator 的将運算符的左運算符作為右運算符的第一個參數,這個提案中的 :: 運算符是将左運算符作為右運算符的 this 參數。不過,通過這個提案其實也能在一定程度上獲得類似的體驗:
function capitalize (str) {
return str[0].toUpperCase() + str.substring(1);
}
function exclaim (str) {
return str + '!'
}
/**
* Pipeline Operator 例子
*/
let result = "hello"
|> capitalize
|> exclaim
result // => 'Hello!'
/**
* 擴充例子
*/
const ::pipe = function (fn) { return fn(this) }
let result = "hello"
::pipe(capitalize)
::pipe(exclaim)
result // => 'Hello!'
值得注意的是,目前 Pipeline Operator 的一個未完成的設計就是不支援 async/await,而對于擴充文法來說,這就有比較大的可自定義空間:
async function post(data) {
const resp = await fetch('127.0.0.1', { method: 'POST', body: data })
return resp.text()
}
/**
* Pipeline Operator 例子
*/
let result = "127.0.0.1:8080"
|> await post // 文法錯誤
/**
* 擴充例子
*/
const ::pipeAwait = async function (fn) { return fn(await this) }
let result = await "hello"
::pipeAwait(post)
::pipeAwait(post)
另外,通過擴充文法我們更可以實作類似自定義機關的方案:
const ::px = Extension.accessor(CSSUnitValue.px) // 非現有 API
1::px // CSSUnitValue {value: 1, unit: "px"}
除了以上所述的使用場景之外,目前提案給予了擴充的聲明獨立命名空間的設計上在會議上有較多的異議。前有例子如 Decorator 的第3個版本,它同樣具有類似的獨立命名空間設計,在委員會上存在強烈的異議。是以雖然提案目前總體進入了 Stage 1,相信這個設計後續會有更多針對性的讨論。
Grouped Accessor
https://github.com/rbuckton/proposal-grouped-and-auto-accessors這是一個來自微軟的代表的提案,提案主要動機是讓 JavaScript 的字段通路器(getter、setter)可以寫在一起,同時給 Decorator 提案設計帶來一個更加工效的字段裝飾方案:
class C {
x {
get() { ... } // equivalent to `get x() { ... }`
set(value) { ... } // equivalent to `set x(value) { ... }`
}
}
const obj = {
x {
get() { ... }
set() { ... }
}
}
class D {
@logger() // 同時裝飾 getter/setter
x {
get() { ... }
set(value) { ... }
}
}
不過以目前 JavaScript 現有的字段通路器聲明文法,較難讓人信服新的方案能夠帶來更好的文法一緻性。如果和現有的 getter/setter 與新的文法一起使用,語義上并沒有增加新的特性,反而卻額外增加了一個寫法,可能會帶來更多問題。這個提案在本次會議上也是在非常多的讨論後才進入 Stage 1。
總結

@JackWorks :如果現在所有的 TC39 提案都寫在一起 JavaScript 會變成什麼樣?
每年進入 TC39 的提案多種多樣,而 TC39 的分階段的流程必然給這些提案與提案的推進者們施加了強大的推進阻力。不過也正是這些分階段性的推進阻力,讓推進者們必須闡明提案所希望解決的問題,提案如何解決問題,提案能否解決問題,是否應該成為 ECMAScript 的一部分等等靈魂發問,才不會讓 ECMAScript 變成一個揉雜了各種突發奇想的産物。
🔥 第十五屆 D2 前端技術論壇 語言架構專場
TC39 核心成員 Ujjwal Sharma 将帶來主題分享 《揭秘TC39:ES2020和ES2021》。重點讨論ES2020和ES2021中令人興奮的新特性,并讨論我們在 TC39中正在進行的一些提案,包括Temporal和Intl.DurationFormat,同時與大家分享我在這個過程中學習到的一些經驗,以及如何将它們應用在自己的工作中。最後為大家介紹 TC39 是如何工作的,以及社群的同學如何參與 TC39 并發表自己的意見。
關注「Alibaba F2E」
把握阿裡巴巴前端新動向