引言
JavaScript 生态持续迭代演进,不断推出提升代码可靠性与资源管理能力的新特性。在 ECMAScript 2024(简称 ES2024)的重要更新中,using
和 await using
声明尤为瞩目。这两种语法借鉴了其他语言(如 C# 的 using
语句)的资源管理模式,提供了结构化的资源管理方案,能在变量退出作用域时自动执行清理操作,从根本上减少 "忘记释放资源" 导致的内存泄漏、连接占用等问题。
截至 2025 年 8 月 20 日,这两个特性已完成标准化,在现代 JavaScript 运行环境(主流浏览器、新版 Node.js)中均能稳定使用。无论是处理文件句柄、数据库连接,还是临时创建的资源对象,它们都能让资源管理更安全、代码更简洁。本文将详细解析其语法规则、工作原理、实战示例,并梳理当前浏览器与 Node.js 的支持情况。
一、using
声明:同步资源的 "自动清洁工"
using
声明专为同步资源管理 设计。它会声明一个块级作用域变量,当变量退出作用域(如代码块结束、函数返回)时,JavaScript 会自动调用资源的 "清理方法",释放其占用的外部资源(如关闭文件、释放锁)或内部状态。
1. 语法规则
using
的语法与 const
类似,要求声明时必须初始化,且变量不可重新赋值(避免意外丢失资源引用):
javascript
using 变量名 = 资源表达式;
- 变量名:需符合 JavaScript 标识符规则,代表要管理的资源对象。
- 资源表达式:必须返回
null
、undefined
,或一个实现了[Symbol.dispose]()
方法的对象 (这是资源能被自动清理的核心 ------[Symbol.dispose]()
就是 "清理逻辑" 的载体)。
若需同时管理多个同步资源,可链式声明(顺序声明,退出作用域时反向清理):
javascript
// 先声明 a,再声明 b;退出作用域时先清理 b,再清理 a
using a = 资源A, b = 资源B;
2. 工作原理:"作用域退出即清理"
using
的核心是确定性清理 ------ 无论代码正常执行结束,还是因异常(如 throw
、return
)提前退出作用域,JavaScript 都会保证调用资源的 [Symbol.dispose]()
方法。
具体流程可拆解为 3 步:
- 声明初始化:执行
using 变量名 = 资源表达式
,创建块级变量,并绑定资源对象。 - 资源使用:在作用域内正常使用资源(如读写文件、调用对象方法)。
- 自动清理:当代码退出作用域(无论正常 / 异常),JavaScript 自动调用
变量名[Symbol.dispose]()
,执行清理逻辑(如删除临时文件、释放内存)。
特别注意:若资源表达式返回 null
或 undefined
,using
会忽略清理(无资源可释放),避免报错。
3. 实战示例:管理临时文件
假设我们需要创建一个临时文件,写入数据后自动删除(避免残留)。用 using
可简化这一流程,无需手动调用 "删除方法":
javascript
// 定义"临时文件"类,实现 [Symbol.dispose]() 方法
class TempFile {
constructor(filePath) {
this.filePath = filePath;
// 初始化:模拟创建临时文件(实际场景可调用 fs.createWriteStream)
console.log(`✅ 临时文件已创建:${this.filePath}`);
}
// 写入数据的方法(业务逻辑)
write(content) {
console.log(`📝 向 ${this.filePath} 写入内容:${content}`);
// 实际场景:this.stream.write(content)
}
// 同步清理方法:必须用 [Symbol.dispose] 命名
[Symbol.dispose]() {
// 清理逻辑:模拟删除临时文件(实际场景可调用 fs.unlinkSync)
console.log(`🗑️ 自动清理:删除临时文件 ${this.filePath}`);
}
}
// 核心逻辑:用 using 管理临时文件
{ // 块级作用域:using 变量仅在此范围内有效
using tempFile = new TempFile('./data/temp.txt');
// 使用资源:调用写入方法
tempFile.write('Hello, using declaration!');
console.log('🔄 正在处理文件数据...');
} // 作用域结束:自动调用 tempFile[Symbol.dispose]()
// 作用域外部:tempFile 已失效,无法访问
console.log('🚀 程序继续执行(临时文件已清理)');
执行结果(清晰看到 "自动清理" 时机):
plaintext
✅ 临时文件已创建:./data/temp.txt
📝 向 ./data/temp.txt 写入内容:Hello, using declaration!
🔄 正在处理文件数据...
🗑️ 自动清理:删除临时文件 ./data/temp.txt
🚀 程序继续执行(临时文件已清理)
即使作用域内抛出异常,清理仍会执行:
javascript
{
using tempFile = new TempFile('./data/error-temp.txt');
tempFile.write('测试异常场景');
throw new Error('模拟业务异常'); // 手动抛出异常
console.log('这行代码不会执行');
} // 仍会自动调用 [Symbol.dispose](),删除临时文件
二、await using
声明:异步资源的 "智能管家"
using
仅能处理同步清理 的资源,而实际开发中,更多资源的清理是异步的(如关闭数据库连接、断开网络请求)------ 这类场景需要 await using
登场。
await using
是 using
的异步扩展,专门用于异步资源管理 ,需配合 async/await
语法使用,能等待异步清理逻辑完成后再继续执行代码。
1. 语法规则
await using
需在 async
函数 / 代码块中使用,语法仅比 using
多一个 await
前缀:
javascript
// 必须在 async 函数/代码块内
await using 变量名 = 资源表达式;
- 资源表达式:需返回 null、undefined,或一个实现了以下方法的对象(优先级从高到低):
[Symbol.asyncDispose]()
:异步清理方法(推荐,专门用于异步场景,返回 Promise);[Symbol.dispose]()
:同步清理方法(兼容场景,若没有异步清理方法则调用)。
同样支持链式声明多个异步资源(退出作用域时反向、按序等待清理):
javascript
await using conn1 = 数据库连接1, conn2 = 数据库连接2;
2. 工作原理:"等待清理完成再下一步"
await using
的核心是异步确定性清理------ 不仅会自动触发清理,还会等待清理操作(如数据库连接关闭)完成后,再执行后续代码,避免 "资源未清理完成就继续操作" 的问题。
具体流程对比 using
,多了 "等待异步清理" 的步骤:
- 声明初始化:在
async
环境中,执行await using
并绑定资源对象。 - 资源使用:调用异步方法使用资源(如
await conn.query()
执行数据库查询)。 - 自动触发清理:退出作用域时,优先调用
[Symbol.asyncDispose]()
(若存在),得到一个 Promise。 - 等待清理完成:JavaScript 自动
await
这个 Promise,等待清理逻辑(如关闭连接)执行完毕。 - 继续后续流程:清理完成后,再执行作用域外部的代码。
3. 实战示例:管理数据库连接
数据库连接是典型的 "异步资源"------ 创建连接需异步(await db.connect()
),关闭连接也需异步(await conn.close()
)。用 await using
可自动管理连接生命周期,避免 "忘记关闭连接导致连接池耗尽" 的问题:
javascript
// 定义"异步数据库连接"类,实现 [Symbol.asyncDispose]()
class AsyncDBConnection {
constructor(dbUrl) {
this.dbUrl = dbUrl;
this.isConnected = false;
// 初始化:模拟异步创建连接(实际场景需在构造后调用 connect())
}
// 异步创建连接的方法(初始化时调用)
async connect() {
// 模拟连接数据库的异步延迟(实际场景:await 数据库驱动的连接方法)
await new Promise(resolve => setTimeout(resolve, 800));
this.isConnected = true;
console.log(`✅ 已连接数据库:${this.dbUrl}`);
}
// 异步执行查询的方法(业务逻辑)
async query(sql) {
if (!this.isConnected) throw new Error('数据库未连接');
console.log(`📊 执行 SQL 查询:${sql}`);
// 模拟查询延迟(实际场景:await this.connection.query(sql))
return await new Promise(resolve =>
setTimeout(() => resolve([{ id: 1, name: 'Alice' }]), 500)
);
}
// 异步清理方法:必须用 [Symbol.asyncDispose] 命名
async [Symbol.asyncDispose]() {
if (!this.isConnected) return;
// 模拟异步关闭连接(实际场景:await this.connection.end())
await new Promise(resolve => setTimeout(resolve, 1000));
this.isConnected = false;
console.log(`🔌 已关闭数据库连接:${this.dbUrl}`);
}
}
// 核心逻辑:用 await using 管理数据库连接
async function fetchUserData() {
// 1. 创建连接并初始化(实际场景可封装为工厂函数:await createDBConnection(url))
const dbConn = new AsyncDBConnection('mysql://user:pass@localhost:3306/mydb');
await dbConn.connect();
// 2. 用 await using 管理连接(作用域:整个 fetchUserData 函数)
await using conn = dbConn;
// 3. 使用资源:执行查询
console.log('🔍 开始查询用户数据...');
const users = await conn.query('SELECT * FROM users LIMIT 1');
console.log(`✅ 查询结果:${JSON.stringify(users)}`);
// 4. 函数结束(作用域退出):自动调用 conn[Symbol.asyncDispose](),并等待连接关闭
}
// 调用函数
fetchUserData().then(() => {
console.log('🚀 数据查询流程完全结束(连接已关闭)');
});
执行结果(注意 "等待连接关闭" 的时序):
plaintext
✅ 已连接数据库:mysql://user:pass@localhost:3306/mydb
🔍 开始查询用户数据...
📊 执行 SQL 查询:SELECT * FROM users LIMIT 1
✅ 查询结果:[{"id":1,"name":"Alice"}]
🔌 已关闭数据库连接:mysql://user:pass@localhost:3306/mydb
🚀 数据查询流程完全结束(连接已关闭)
若查询过程中抛出异常,连接仍会被自动关闭:
javascript
async function fetchWithError() {
const dbConn = new AsyncDBConnection('mysql://user:pass@localhost:3306/mydb');
await dbConn.connect();
await using conn = dbConn;
await conn.query('SELECT * FROM users');
throw new Error('模拟查询后异常'); // 抛出异常
}
fetchWithError().catch(err => {
console.error('❌ 出错:', err.message);
// 异常捕获后,仍会等待 conn[Symbol.asyncDispose]() 执行完毕
}).finally(() => {
console.log('🔚 流程结束(连接已清理)');
});
三、底层原理与实现(规范语义、反糖与可运行代码)
本节从规范抽象操作、错误模型、事件循环时序到可运行的参考实现,系统阐释 using
/ await using
的底层机制。
3.1 规范语义总览(核心元素)
- 新增符号方法
Symbol.dispose
:同步清理方法(返回值忽略)。Symbol.asyncDispose
:异步清理方法(返回 Promise)。
- 新增错误类型
SuppressedError
:当"清理阶段"发生多个错误时,用于"保留主错误、压制附加错误"的链式封装。
- 两个关键抽象操作(来自规范)
AddDisposableResource(stack, V, hint)
:- 若
V
为null/undefined
,记录空资源(无清理)。 - 若
V
非对象,抛TypeError
。 - 选择清理方法:
using
:必须存在V[Symbol.dispose]
,否则抛错。await using
:优先V[Symbol.asyncDispose]
,否则退回V[Symbol.dispose]
,均不存在则抛错。
- 将资源记录压入当前作用域的"资源栈"(LIFO)。
- 若
DisposeResources(stack)
:- 退出作用域时执行(正常/异常退出皆执行),按栈"后进先出"依次清理。
- 对于
await using
,若采用Symbol.asyncDispose
会等待其 Promise。 - 错误合并策略使用
SuppressedError
链接多个错误,最后重新抛出。
结论:using
/await using
是"确定性清理",并且严格保证 LIFO 顺序与错误合并的可预期行为。
3.2 行为算法要点(贴近规范的伪代码)
javascript
// 资源登记(进入作用域时)
function AddDisposableResource(stack, value, isAwaitUsing) {
if (value == null) { // null 或 undefined
stack.push({ kind: 'none' });
return;
}
if (typeof value !== 'object' && typeof value !== 'function') {
throw new TypeError('Resource must be an object or function');
}
let method, isAsyncMethod = false;
if (isAwaitUsing) {
if (typeof value[Symbol.asyncDispose] === 'function') {
method = value[Symbol.asyncDispose];
isAsyncMethod = true;
} else if (typeof value[Symbol.dispose] === 'function') {
method = value[Symbol.dispose];
} else {
throw new TypeError('Async-disposable resource method missing');
}
} else {
if (typeof value[Symbol.dispose] === 'function') {
method = value[Symbol.dispose];
} else {
throw new TypeError('Disposable resource method missing');
}
}
stack.push({ kind: 'resource', value, method, isAsyncMethod });
}
// 资源释放(离开作用域时)
async function DisposeResources(stack, pendingError) {
let error = pendingError; // 可能为 undefined
const SuppressedErrorCtor = globalThis.SuppressedError;
for (let i = stack.length - 1; i >= 0; --i) {
const rec = stack[i];
if (rec.kind !== 'resource') continue;
try {
const ret = rec.method.call(rec.value);
if (rec.isAsyncMethod) await ret;
} catch (e) {
if (error === undefined) {
error = e;
} else if (typeof SuppressedErrorCtor === 'function') {
error = new SuppressedErrorCtor(error, e, 'An error was suppressed during disposal');
} else {
// 退化策略:用 AggregateError 模拟(不完全等价)
error = new AggregateError([error, e], 'Multiple errors during disposal');
}
}
}
if (error !== undefined) throw error;
}
要点:
- LIFO:最后登记的资源最先被清理(保证"依赖先释放"的惯用语义)。
- 方法选择:
await using
优先异步清理方法,其次同步;using
只能同步。 - 错误策略:所有清理都会尝试执行,错误被合并,最后统一抛出。
3.3 与 try/finally 的等价反糖(Desugar)
using
/await using
可理解为"带有资源栈+错误合并的 try/finally"。下例直观展示语义等价:
源代码(语法糖):
javascript
{
using a = getA(), b = getB();
// ... 使用 a/b
}
等价反糖(概念化):
javascript
{
const __stack = [];
let __thrown;
try {
const a = getA(); AddDisposableResource(__stack, a, /*await?*/ false);
const b = getB(); AddDisposableResource(__stack, b, /*await?*/ false);
// ... 使用 a/b
} catch (e) {
__thrown = e;
throw e;
} finally {
// 同步 using:不需要 await,但要合并错误并在最后抛出
// 伪码:DisposeResourcesSync(__stack, __thrown);
// 为保持统一思想,亦可使用异步版本并在调用处 await
}
}
await using
的 finally 会"等待清理完成":
javascript
async function f() {
const __stack = [];
let __thrown;
try {
const conn = await openConn();
AddDisposableResource(__stack, conn, /*await?*/ true);
// ... await 使用 conn
} catch (e) {
__thrown = e;
throw e;
} finally {
await DisposeResources(__stack, __thrown);
}
}
3.4 事件循环与微任务时序(为什么"await using"会等待)
using
:清理是同步调用;作用域一结束,清理立刻执行,后续同步代码继续。await using
:若选择到Symbol.asyncDispose
,清理返回 Promise;规范语义要求等待该 Promise 再继续后续流程。等待期间会让出执行权,其他微任务可能先执行。
验证示例(时序示意):
javascript
class R {
async [Symbol.asyncDispose]() {
console.log('close start');
await Promise.resolve().then(() => console.log('microtask inside dispose'));
console.log('close end');
}
}
(async () => {
console.log('A');
await using r = new R();
console.log('B');
})();
console.log('C');
Promise.resolve().then(() => console.log('D'));
// 可能的输出:
// A
// C
// D
// close start
// microtask inside dispose
// close end
// B
说明:await using
的清理在离开作用域时被等待,清理期间微任务可穿插执行,直至清理完毕后才继续输出 B
。
3.5 参考实现:最小可运行代码(在不支持语法的环境复现语义)
注意:语法本身无法 polyfill,这里给出"等价运行时"与"反糖写法",用于老环境或工具链自定义转译。
javascript
// 最小运行时:登记与释放
const SuppressedErrorShim = class extends Error {
constructor(error, suppressed, message) {
super(message || 'SuppressedError');
this.name = 'SuppressedError';
this.error = error;
this.suppressed = suppressed;
}
};
const SuppressedErrorCtor = globalThis.SuppressedError || SuppressedErrorShim;
function addDisposableResource(stack, value, isAwaitUsing) {
if (value == null) { stack.push({ kind: 'none' }); return; }
const t = typeof value;
if (t !== 'object' && t !== 'function') throw new TypeError('Resource must be object/function');
let method, isAsyncMethod = false;
if (isAwaitUsing) {
if (typeof value[Symbol.asyncDispose] === 'function') {
method = value[Symbol.asyncDispose]; isAsyncMethod = true;
} else if (typeof value[Symbol.dispose] === 'function') {
method = value[Symbol.dispose];
} else {
throw new TypeError('Async-disposable resource method missing');
}
} else {
if (typeof value[Symbol.dispose] === 'function') {
method = value[Symbol.dispose];
} else {
throw new TypeError('Disposable resource method missing');
}
}
stack.push({ kind: 'resource', value, method, isAsyncMethod });
}
function disposeResourcesSync(stack, pendingError) {
let error = pendingError;
for (let i = stack.length - 1; i >= 0; --i) {
const rec = stack[i];
if (rec.kind !== 'resource') continue;
try {
// 同步模式不等待;如果 method 实际返回 Promise,语义上不等价于 await using
rec.method.call(rec.value);
} catch (e) {
error = error === undefined
? e
: new SuppressedErrorCtor(error, e, 'An error was suppressed during disposal');
}
}
if (error !== undefined) throw error;
}
async function disposeResourcesAsync(stack, pendingError) {
let error = pendingError;
for (let i = stack.length - 1; i >= 0; --i) {
const rec = stack[i];
if (rec.kind !== 'resource') continue;
try {
const r = rec.method.call(rec.value);
if (rec.isAsyncMethod) await r;
} catch (e) {
error = error === undefined
? e
: new SuppressedErrorCtor(error, e, 'An error was suppressed during disposal');
}
}
if (error !== undefined) throw error;
}
// 使用示例:等价 "using"
(function demoSync() {
class Temp {
constructor(n) { this.n = n; console.log('open', n); }
[Symbol.dispose]() { console.log('dispose', this.n); }
}
const __stack = [];
try {
const a = new Temp('A'); addDisposableResource(__stack, a, false);
const b = new Temp('B'); addDisposableResource(__stack, b, false);
console.log('work');
} finally {
disposeResourcesSync(__stack);
}
// 输出:open A, open B, work, dispose B, dispose A
})();
// 使用示例:等价 "await using"
(async function demoAsync() {
class Conn {
async [Symbol.asyncDispose]() {
await new Promise(r => setTimeout(r, 10));
console.log('conn closed');
}
async query() { return 42; }
}
const __stack = [];
try {
const c = new Conn(); addDisposableResource(__stack, c, true);
console.log('answer =', await c.query());
} finally {
await disposeResourcesAsync(__stack);
}
// 输出顺序保证:query 完成 -> await 清理完成 -> 退出
})();
要点提示:
- 同步/异步两种释放函数分别使用,避免在同步上下文误用异步清理。
- 错误模型遵循"尽可能清理 + 最后抛出合并错误"。
3.6 与 DisposableStack/AsyncDisposableStack 的关系
DisposableStack
/ AsyncDisposableStack
是同一提案中的"编程式资源栈":
- 作用:手动将清理函数或可处置对象压入栈,最后一次性清理(LIFO)。
- 与
using
的关系:语义一致、形态不同。using
是声明式语法糖;DisposableStack
更适合需要跨语句/动态管理的一组资源。
示例:
javascript
// 同步
const stack = new DisposableStack();
const a = stack.use(new Temp('A')); // 自动选用 a[Symbol.dispose]
const b = stack.use(new Temp('B'));
try {
// ... 使用 a/b
} finally {
stack.dispose(); // 同步 LIFO 清理
}
// 异步
const astack = new AsyncDisposableStack();
const conn = await astack.useAsync(await openConn()); // 选用 asyncDispose 或退回 dispose
try {
// ... await 使用 conn
} finally {
await astack.disposeAsync(); // 异步 LIFO 清理
}
3.7 常见边界与陷阱
- 可空资源:
null/undefined
会被忽略,不会抛错也不会清理。 - 原始值:若资源表达式结果是原始值(number/string 等),抛
TypeError
。 - 方法缺失:
using
需要[Symbol.dispose]
;await using
至少需要[Symbol.asyncDispose]
或[Symbol.dispose]
之一。
- 清理顺序:始终 LIFO,设计多资源依赖时按依赖顺序声明,退出时自动反向释放。
- 错误合并:清理阶段的错误不会提前中止后续清理;最终以
SuppressedError
(或退化合并)抛出。 - 事件循环:
await using
的异步清理会"等待",期间微任务可能插队,确保资源真正释放后才继续执行作用域外代码。
四、浏览器与 Node.js 支持情况(来源:MDN)


五、总结与最佳实践
using
和 await using
填补了 JavaScript 原生 "结构化资源管理" 的空白,相比传统的 "手动调用清理方法"(如 try/finally
),优势显著:
- 代码更简洁:无需嵌套
try/finally
,减少样板代码; - 可靠性更高:强制自动清理,避免 "忘记释放资源" 导致的泄漏;
- 异步友好:
await using
完美适配异步场景,解决 "异步清理未完成" 的问题。
最佳实践建议
- 明确资源类型:同步资源用
using
,异步资源优先用await using
(并实现[Symbol.asyncDispose]()
)。 - 链式声明顺序:多个资源链式声明时,按 "依赖顺序" 排列(如先声明数据库连接,再声明基于连接的事务),退出时反向清理,避免依赖冲突。
- 兼容旧环境:若需支持 Node.js v22 以下或 Safari 18 以下,需配置 Babel 转译 + polyfill。
- 避免重新赋值:
using
/await using
变量不可重新赋值(类似const
),若强行赋值会报错,需提前规划资源引用。
通过这两个特性,JavaScript 开发者能更轻松地管理各类资源,写出更健壮、更易维护的代码。建议在新项目中积极尝试,尤其是涉及文件操作、数据库连接、网络请求等场景 ------ 让 "自动清理" 成为常态,告别资源泄漏的烦恼。