你以为 try/finally 已经够用了?今天 using 关键字把这件事彻底原生化了

写过 JavaScript 的人都踩过这个坑——每次写 try/finally 只是为了释放一个资源,今天 using 关键字把这件事彻底原生化了。

你写过多少次这样的代码?

const file = fs.openSync('config.json', 'r');
try {
  const content = file.readFileSync();
  return JSON.parse(content);
} finally {
  file.close(); // 这行漏了,文件句柄就泄漏了
}

一个资源的获取和释放,分散在函数的两头。中间一旦加了 early return、加了 throw 路径,finally 块就要跟着改。漏一行,资源就泄漏了。

这个问题存在了二十多年。终于,JavaScript 自己学会清理了。

using 怎么用

ES2026(TC39 Stage 4,2025 年 5 月正式固化)引入了 using 声明。对象只要带 [Symbol.dispose]() 方法,用 using 声明后,作用域退出时自动调用:

import { open } from 'node:fs/promises';

async function readConfig() {
  await using file = await open('config.json', 'r');
  const { buffer } = await file.read();
  return JSON.parse(buffer);
} // file[Symbol.asyncDispose]() 自动调用,无论正常返回还是抛异常

await using 处理异步清理(数据库连接、流关闭)。using 处理同步清理(文件句柄、锁)。

多个资源按声明逆序释放

await using conn = await openConnection(); // 第二个释放
await using tx = await conn.beginTransaction(); // 第一个释放
await tx.query('SELECT 1');
// 退出时:先关 tx,再关 conn——依赖顺序自动保证

自己写一个 Disposable

只要对象有 [Symbol.dispose](),就能参与 using

class ManagedTimer {
  #id;
  constructor(fn, ms) { this.#id = setTimeout(fn, ms); }
  [Symbol.dispose]() { clearTimeout(this.#id); }
}

function processWithTimeout(fn, ms) {
  using _timer = new ManagedTimer(fn, ms);
  // 无论 processItems 怎么退出,timer 都会被清理
}

事件监听器也可以这么包装:

function onHover(el, enter, leave) {
  el.addEventListener('mouseenter', enter);
  el.addEventListener('mouseleave', leave);
  return {
    [Symbol.dispose]() {
      el.removeEventListener('mouseenter', enter);
      el.removeEventListener('mouseleave', leave);
    }
  };
}

DisposableStack:动态资源组合

运行时要注册多个未知数量的资源?DisposableStack 来帮忙:

async function processFiles(paths) {
  await using stack = new AsyncDisposableStack();
  const handles = await Promise.all(
    paths.map(p => stack.use(await open(p, 'r')))
  );
  await transformAll(handles);
} // stack 退出时,所有文件句柄按逆序全部关闭

stack.use(resource) 注册并返回资源,stack.move() 可以把资源转移到另一个栈,防止自动释放。

SuppressedError:cleanup 失败不再吞掉

如果你的业务代码和 cleanup 代码同时抛异常,JavaScript 会把两个错误包在一个 SuppressedError 里,不会再有任何异常被静默吞掉。

现在在哪能用

环境 支持版本
Chrome / Edge 134+(2025 年 3 月)
Firefox 141+(当前稳定版 152)
Safari 18.2+(桌面)
Node.js 24+(V8 13.6)
Deno 1.37+
Bun 原生支持
TypeScript 5.2+(会转译到 older target)
Babel 7.28+(默认开启)

桌面浏览器全覆盖,移动端 Safari(iOS)还在追赶。生产环境用 TypeScript 或 Babel 编译即可,行为完全正确。

怎么上手

搜一下你代码库里有多少 finally {.*close|release|dispose 的模式——那些就是最优先可以迁移的目标。给资源类加上 [Symbol.dispose](),把 cleanup 从 finally 块移到资源获取那一行,作用域自己会处理剩下的。

这不是魔法,是 JavaScript 和资源对象之间的一个明确契约:实现了 Symbol.dispose,引擎负责在正确的时机替你调用。try/finally 解决的问题,using 用一行声明解决了;try/finally 没解决好的(多资源顺序、cleanup 失败吞异常),using 反而处理得更干净。

评论区

0 条评论

登录后可评论。

阿柯·前端架构 12 阅读