你知道using 和 await using这两个js新特性吗?

引言

JavaScript 生态持续迭代演进,不断推出提升代码可靠性与资源管理能力的新特性。在 ECMAScript 2024(简称 ES2024)的重要更新中,usingawait using 声明尤为瞩目。这两种语法借鉴了其他语言(如 C# 的 using 语句)的资源管理模式,提供了结构化的资源管理方案,能在变量退出作用域时自动执行清理操作,从根本上减少 "忘记释放资源" 导致的内存泄漏、连接占用等问题。

截至 2025 年 8 月 20 日,这两个特性已完成标准化,在现代 JavaScript 运行环境(主流浏览器、新版 Node.js)中均能稳定使用。无论是处理文件句柄、数据库连接,还是临时创建的资源对象,它们都能让资源管理更安全、代码更简洁。本文将详细解析其语法规则、工作原理、实战示例,并梳理当前浏览器与 Node.js 的支持情况。

一、using 声明:同步资源的 "自动清洁工"

using 声明专为同步资源管理 设计。它会声明一个块级作用域变量,当变量退出作用域(如代码块结束、函数返回)时,JavaScript 会自动调用资源的 "清理方法",释放其占用的外部资源(如关闭文件、释放锁)或内部状态。

1. 语法规则

using 的语法与 const 类似,要求声明时必须初始化,且变量不可重新赋值(避免意外丢失资源引用):

javascript 复制代码
using 变量名 = 资源表达式;
  • 变量名:需符合 JavaScript 标识符规则,代表要管理的资源对象。
  • 资源表达式:必须返回 nullundefined,或一个实现了 [Symbol.dispose]() 方法的对象 (这是资源能被自动清理的核心 ------[Symbol.dispose]() 就是 "清理逻辑" 的载体)。

若需同时管理多个同步资源,可链式声明(顺序声明,退出作用域时反向清理):

javascript 复制代码
// 先声明 a,再声明 b;退出作用域时先清理 b,再清理 a
using a = 资源A, b = 资源B;

2. 工作原理:"作用域退出即清理"

using 的核心是确定性清理 ------ 无论代码正常执行结束,还是因异常(如 throwreturn)提前退出作用域,JavaScript 都会保证调用资源的 [Symbol.dispose]() 方法。

具体流程可拆解为 3 步:

  1. 声明初始化:执行 using 变量名 = 资源表达式,创建块级变量,并绑定资源对象。
  2. 资源使用:在作用域内正常使用资源(如读写文件、调用对象方法)。
  3. 自动清理:当代码退出作用域(无论正常 / 异常),JavaScript 自动调用 变量名[Symbol.dispose](),执行清理逻辑(如删除临时文件、释放内存)。

特别注意:若资源表达式返回 nullundefinedusing 会忽略清理(无资源可释放),避免报错。

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 usingusing 的异步扩展,专门用于异步资源管理 ,需配合 async/await 语法使用,能等待异步清理逻辑完成后再继续执行代码。

1. 语法规则

await using 需在 async 函数 / 代码块中使用,语法仅比 using 多一个 await 前缀:

javascript 复制代码
// 必须在 async 函数/代码块内
await using 变量名 = 资源表达式;
  • 资源表达式:需返回 null、undefined,或一个实现了以下方法的对象(优先级从高到低):
    1. [Symbol.asyncDispose]():异步清理方法(推荐,专门用于异步场景,返回 Promise);
    2. [Symbol.dispose]():同步清理方法(兼容场景,若没有异步清理方法则调用)。

同样支持链式声明多个异步资源(退出作用域时反向、按序等待清理):

javascript 复制代码
await using conn1 = 数据库连接1, conn2 = 数据库连接2;

2. 工作原理:"等待清理完成再下一步"

await using 的核心是异步确定性清理------ 不仅会自动触发清理,还会等待清理操作(如数据库连接关闭)完成后,再执行后续代码,避免 "资源未清理完成就继续操作" 的问题。

具体流程对比 using,多了 "等待异步清理" 的步骤:

  1. 声明初始化:在 async 环境中,执行 await using 并绑定资源对象。
  2. 资源使用:调用异步方法使用资源(如 await conn.query() 执行数据库查询)。
  3. 自动触发清理:退出作用域时,优先调用 [Symbol.asyncDispose]()(若存在),得到一个 Promise。
  4. 等待清理完成:JavaScript 自动 await 这个 Promise,等待清理逻辑(如关闭连接)执行完毕。
  5. 继续后续流程:清理完成后,再执行作用域外部的代码。

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)
      • Vnull/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)

五、总结与最佳实践

usingawait using 填补了 JavaScript 原生 "结构化资源管理" 的空白,相比传统的 "手动调用清理方法"(如 try/finally),优势显著:

  1. 代码更简洁:无需嵌套 try/finally,减少样板代码;
  2. 可靠性更高:强制自动清理,避免 "忘记释放资源" 导致的泄漏;
  3. 异步友好:await using 完美适配异步场景,解决 "异步清理未完成" 的问题。

最佳实践建议

  1. 明确资源类型:同步资源用 using,异步资源优先用 await using(并实现 [Symbol.asyncDispose]())。
  2. 链式声明顺序:多个资源链式声明时,按 "依赖顺序" 排列(如先声明数据库连接,再声明基于连接的事务),退出时反向清理,避免依赖冲突。
  3. 兼容旧环境:若需支持 Node.js v22 以下或 Safari 18 以下,需配置 Babel 转译 + polyfill。
  4. 避免重新赋值:using/await using 变量不可重新赋值(类似 const),若强行赋值会报错,需提前规划资源引用。

通过这两个特性,JavaScript 开发者能更轻松地管理各类资源,写出更健壮、更易维护的代码。建议在新项目中积极尝试,尤其是涉及文件操作、数据库连接、网络请求等场景 ------ 让 "自动清理" 成为常态,告别资源泄漏的烦恼。

六、参考与延伸阅读

相关推荐
恋猫de小郭1 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅8 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅9 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅10 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊10 小时前
jwt介绍
前端