如果你用過 WeakSet 或 WeakMap,其實已經見過一種“隱式資源管理”的思想。
WeakSet / WeakMap 的 “weak(弱引用)”含義是:它們對值(或 key)的引用不會阻止垃圾回收(GC)。當某個對象在程序里不再被其他地方引用時,它就有機會被回收;一旦被回收,WeakSet/WeakMap 里對應的條目也可能隨之消失。
因此,WeakSet/WeakMap 只能存放可被 GC 的值:對象引用,以及未注冊到全局 Symbol 注冊表的 Symbol。比如嘗試把 true 這種原始值放進 WeakSet,會報錯:
const theWeakSet = new WeakSet([true]);
WeakMap 的典型用途是:給某個對象“外掛”一些關聯數據,但又不把數據真的掛在對象本身上,同時也不阻止對象被 GC:
const theObject = {};
const theWeakMap = new WeakMap([[theObject, "A string, say, describing the object."]]);
console.log(theWeakMap.get(theObject));
看上去很美:對象沒了,關聯數據也應該跟著消失——像極了“代碼會自己打掃衛生”。
不過作者也提醒:垃圾回收何時發生是不確定的。也就是說,即便對象已經沒有其他引用,你也不能保證它立刻被回收;因此 WeakMap 里的條目也不一定馬上消失。
隱式資源管理的好處是“你不用管”;壞處是“你也管不了”。
顯式資源管理(Explicit resource management)
顯式資源管理并不是讓你手動管理內存(GC 依然是引擎的事),它解決的是另一類更常見、更工程化的問題:
當某個資源“用完了”,我們希望能確定執行一組清理動作。
這里的“資源”可以理解為:有明確“結束狀態”的對象。例如:文件句柄、WebSocket 連接、流、鎖、訂閱、觀察者、以及各種需要 close() / disconnect() / abort() 的東西。
作者用 generator 舉例,說明“生命周期結束時執行清理”在 JS 里并不陌生:generator 的 done 會在迭代結束時變成 true;并且你可以在 generator 內用 try...finally 來保證收尾邏輯被執行。
一個簡化示例:
function* generatorFunction() {
try {
yield true;
yield false;
} finally {
console.log("All done.");
}
}
const generatorObject = generatorFunction();
console.log(generatorObject.next());
console.log(generatorObject.next());
console.log(generatorObject.next());
如果你提前調用 return(),也會走到 finally:
console.log(generatorObject.return());
作者把這種“我明確地讓它現在結束并清理”的方式稱為命令式(imperative)資源管理:比如你手動調用 close()、abort()、disconnect()。
問題在于:這些清理方法在不同 API 里名字五花八門,而我們做的事卻高度一致——“把它關掉、清理掉”。于是提案引入了一個統一約定:
以 generator 為例,它可以把 [Symbol.dispose] 標準化為對 return() 的包裝:
console.log(generatorObject[Symbol.dispose]());
這在 generator 場景里看起來變化不大,但意義很大:它為“任何需要清理的資源”提供了統一入口。
using:聲明式資源管理
有了統一的 [Symbol.dispose](),提案就可以再向前一步:提供聲明式(declarative)資源管理。
也就是:不再靠“記得手動調用 dispose”,而是把資源的清理動作綁定到作用域生命周期上。
提案為此引入了一個新的變量聲明關鍵字:using。
using 聲明是塊級作用域(和 const / let 類似)。
using 聲明的綁定不可重新賦值(像 const)。
當代碼執行離開該作用域時,引擎會自動調用資源的 disposer,即 resource[Symbol.dispose]()。
一個最小示例:
{
using theObject = {
[Symbol.dispose]() {
console.log("All done.");
},
};
// 離開作用域時,會自動輸出 "All done."
}
需要注意:using 不是“更酷的 const”。它只能用于:
比如這樣會報錯(因為 {} 沒有 disposer):
{
using theObject = {};
}
并且 using 必須處在某個明確的作用域中(塊、函數體、靜態初始化塊、for/for-of/for-await-of 的初始化部分,或模塊頂層),否則它就沒有“離開作用域”這一刻,也就失去了意義。
回到文章前面那個“把文件開著就走了”的 generator 場景:如果用 using 來聲明 generator 對象,那么在離開作用域時就會自動觸發清理:
{
function* generatorFunction() {
console.log("Open a file.");
try {
yield true;
yield false;
} finally {
console.log("Close the file.");
}
}
using generatorObject = generatorFunction();
console.log(generatorObject.next());
}
同理,如果你寫一個類實例需要“用完自動收尾”,也可以直接實現 [Symbol.dispose]():
class TheClass {
theFile;
constructor(theFile) {
this.theFile = theFile;
console.log(`Open ${theFile}`);
}
[Symbol.dispose]() {
console.log(`Close ${this.theFile}`);
}
}
const theFile = "./some-file";
if (theFile) {
using fileOpener = new TheClass(theFile);
console.log(`Do things with ${fileOpener.constructor.name}, then...`);
}
現狀與落地
作者提到:該提案已進入 TC39 Stage 3(推薦實現),并且大多數瀏覽器已經支持(Safari 仍缺席)。你可以在 caniuse 上查看:
當然,Stage 3 仍然意味著“可能還有語法細節會變”,所以更適合現在就開始在實驗/非生產環境熟悉它。
作者最后把這件事總結為一種很樸素、但非常工程化的收益:
JS 終于開始從“全靠自覺的清理”走向“語言級別幫助你不忘記清理”。
參考文章:原文鏈接?