TC39JavaScript新語法usingExplicit Resource Management

TC39 新語法深度解析(二):Explicit Resource Management — using 關鍵字讓資源自動釋放

整理 TC39 Explicit Resource Management(Stage 4)的 using 和 await using 關鍵字。Symbol.dispose、DisposableStack、與傳統 try-finally 的完整比較。

· 4 分鐘閱讀

這是 TC39 新語法深度解析系列的第二篇。忘記關閉檔案、遺漏 DB 連線、視窗外監聽忘記移除——這些都是每個工程師曾經踩過的坑。Explicit Resource Management 是 TC39 對這個問題的標準化答案。


問題:忘記 cleanup 是代價昂貴的錯誤

// 過去的做法:try-finally
async function processFile(path: string) {
  const file = await fs.open(path, 'r');
  try {
    const content = await file.read();
    return content;
  } finally {
    await file.close();  // 終於執行
  }
}

// 問題:如果在 try 裡有 early return 或拋出錯誤
// finally 仍然會執行——這是對的
// 但如果錯誤發生在 await file.read() 和 await file.close() 中間
// 某些資源釋放順序可能不正確

更糟的是,當你需要管理多個資源時,try-finally 會快速失控:

async function multiResource() {
  const conn = await db.connect();
  const file = await fs.open('data.json', 'r');
  const socket = await net.connect('api.example.com', 443);
  
  try {
    // 實際工作...
  } finally {
    // 必須按正確順序釋放:後進先出
    if (socket) await socket.close();
    if (file) await file.close();
    if (conn) await conn.close();
  }
}

using 的語法:資源自動釋放

基本用法

// using 關鍵字:離開作用域時,自動呼叫 .dispose()
function readConfig() {
  using file = { path: 'config.json' };
  // 模擬:打開文件
  console.log('Reading config...');
  // 離開函式時,file[Symbol.dispose]() 自動呼叫
}

const content = readConfig();
// console output:
// Reading config...
// Closing config.json (自動)

實際與 async 資源

// await using:用於 async 資源
async function fetchUserData(userId: string) {
  await using conn = await db.connect();  // 離開時自動 await conn[Symbol.asyncDispose]()

  const user = await conn.query(
    'SELECT * FROM users WHERE id = ?',
    [userId]
  );

  return user;
  // conn.dispose() 在函式結束時自動呼叫
}

Symbol.dispose 與 Symbol.asyncDispose

任何物件只要實作了 [Symbol.dispose]() 方法,就可以用 using 管理:

// 資源類別:實作 Symbol.dispose
class DatabaseConnection {
  async [Symbol.asyncDispose]() {
    console.log('Closing database connection...');
    await this.client.end();
  }
}

async function query() {
  await using db = new DatabaseConnection(config);
  const result = await db.query('SELECT 1');
  return result;
  // [Symbol.asyncDispose] 在這裡自動呼叫
}

DisposableStack:管理多個資源

當你需要一次管理多個資源,DisposableStack 是最優雅的解決方案:

function batchProcess() {
  // 建立一個堆疊來管理多個資源
  using stack = new DisposableStack();

  const file = stack.use(openFile('data.csv'));
  const db = stack.use(await dbPool.connect());
  const cache = stack.use(new LRUCache());

  // 如果中途出錯,stack.use() 確保所有資源在正確順序下被釋放
  const result = process(file, db, cache);

  return result;
  // 所有資源自動釋放(後進先出)
}

DisposableStackuse() 方法讓你可以在一個 stack 裡追蹤多個資源,並確保它們**以正確順序(後進先出)**被釋放。


實用範例:從檔案 I/O 到 Canvas

檔案處理

// before:try-finally 冗長
async function readAndProcessFile(path: string) {
  let file: FileHandle;
  try {
    file = await fs.open(path, 'r');
    const content = await file.read();
    return process(content);
  } finally {
    if (file) await file.close();
  }
}

// after:using 簡潔
async function readAndProcessFile(path: string) {
  await using file = await fs.open(path, 'r');
  return process(await file.read());
  // file.close() 自動
}

Canvas Context

// Canvas 2D context 的 restore 總是忘記加?
function drawChart(canvas: HTMLCanvasElement) {
  const ctx = canvas.getContext('2d');

  using _ = {
    [Symbol.dispose]() {
      ctx.restore();  // 總是執行,無論正常還是異常退出
    }
  };

  ctx.save();
  ctx.translate(100, 100);
  ctx.rotate(Math.PI / 4);

  drawSomething(ctx);

  // _[Symbol.dispose]() 在這裡自動執行,ctx.restore() 必定被呼叫
}

TypeScript 5.2+ 支援

usingawait using 現已進入 TypeScript 5.2+:

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022"]
  }
}
// 確認 TypeScript 版本
// npx tsc --version  // 需要 5.2+

function example() {
  using resource = createResource();
  // 使用 resource...
}
// [Symbol.dispose]() 在函式結束時自動呼叫

與 try-finally 的比較

維度try-finallyusing
語法簡潔度需要明確寫 finally自動,無需 extra block
多資源管理需要巢狀 try 或手動管理DisposableStack
資源釋放順序手動控制,容易出錯自動後進先出
編譯器優化無特殊處理可被編譯器優化
可組合性困難DisposableStack 可以组合

總結

using 是那種「一旦用過就不想回去」的語法。它的價值:

  • 編譯器強制執行:不再可能忘記 cleanup
  • 程式碼更短:不需要 try-finally 包裝
  • 資源釋放順序正確:後進先出由 runtime 保證
  • 可組合:DisposableStack 讓多資源管理優雅

這是 TC39 新語法深度解析系列的第二篇。下一篇:Set 新方法 — unionintersectiondifference 終於原生支援。


延伸閱讀


本文基於 TC39 Explicit Resource Management 提案(Stage 4)整理,TypeScript 5.2+ 已支援。