写了八年 try/finally,今天 Safari 终于把 using 关键字放出来了——这件事把 JavaScript 的资源管理彻底变了
你写过多少次这样的代码?
const file = await openFile("data.json");
try {
const data = await file.read();
process(data);
} finally {
await file.close();
}
三行逻辑,七行清理。finally 永远在远处等你,每次加新 return 或者新 throw 都要记得把它一起扩进去。漏一次,文件句柄就漂在外面。
这个局面,今天变了。
Safari Technology Preview 250 在 2026 年 8 月 13 日正式支持了 using 和 await using 关键字。这是 TC39 Explicit Resource Management 提案的核心特性,Stage 4,ES2026 正式成员。
三大引擎全部就位
- Chrome 134(2025 年 3 月)
- Firefox 141(2025 年 7 月)
- Safari(STP 250,2026 年 8 月)
从今天起,using 关键字在所有主流浏览器里都是原生能力,不需要任何 polyfill。TypeScript 5.2 从 2023 年就开始做类型支持了,但那时候 Safari 不支持,编译出来还是 try/finally。现在可以直译了。
它到底怎么工作的
当你写 using resource = getFile(),JavaScript 引擎会把这个变量绑定到当前作用域。当作用域退出——正常返回、提前 return、或者抛出异常——引擎会自动调用 resource[Symbol.dispose]()。
const getFile = async (path) => {
const handle = await open(path);
return {
handle,
[Symbol.asyncDispose]: async () => await handle.close()
};
};
async function process() {
await using file = getFile("data.json");
const data = await file.handle.read();
// 不用写 finally,文件在这里自动关闭
}
多个 using 声明会按逆序释放——最后声明的最先关闭。这和栈展开的逻辑一样,保证依赖关系安全。
DisposableStack:动态资源组
有时候你在写循环,循环多少次在编译时不知道,DisposableStack 就是给这种情况准备的:
async function processAll(paths) {
await using stack = new AsyncDisposableStack();
const handles = paths.map(p => stack.use(openFile(p)));
await transformAll(handles);
// 所有文件在这里逆序关闭,无论 transformAll 抛不抛异常
}
两个容易搞混的地方
第一,using 不等于自动释放内存。垃圾回收回收的是内存,而 Symbol.dispose 处理的是非内存资源——文件句柄、数据库连接、锁、事件监听器订阅、计时器。这些 GC 不管,你以前只能靠 try/finally,现在靠作用域。
第二,using 和 await using 是两个东西。同步资源用 using,清理函数不返回 Promise。异步资源用 await using,引擎会 await 完清理再继续。如果你的清理操作里有 await——比如关闭数据库连接、flush 写缓冲区——必须用 await using。
对前端工程师意味着什么
这不只是后端的事。前端工程师每天都在用非内存资源:
- WebSocket 连接
- BroadcastChannel
- EventListener 订阅
- setTimeout/setInterval
- Service Worker 注册
- MediaStream 轨道
这些在以前全靠手动 cleanup 函数,现在可以写成可 Dispose 对象然后用 using 声明:
function trackClick(el, handler) {
el.addEventListener("click", handler);
return {
[Symbol.dispose]() { el.removeEventListener("click", handler); }
};
}
function setup() {
using _a = trackClick(buttonA, onA);
using _b = trackClick(buttonB, onB);
// 组件卸载时两个监听器自动清除
}
下一步
- 检查你的 TypeScript target。如果用的是 ES2026,TypeScript 会直接 emit using 而不转译。如果 target 更低,它会转成 try/finally——行为正确,只是代码更长。
- 把你项目中手写的 cleanup 函数挑出来,给它们实现 Symbol.dispose。EventListener cleanup 是最容易迁移的一类。
- 注意 iOS Safari。STP 250 是桌面版 Safari,iOS Safari 16.4 还没有。这是渐进增强的场景,生产代码里用 using 之前加上 @supports 检测。
JavaScript 终于有了确定性资源清理。这不是语法糖,这是把一种类型的 bug 从语言里永久删掉。
评论区
登录后可评论。