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

六、参考与延伸阅读

相关推荐
前端W几秒前
腾讯地图组件使用说明文档
前端
页面魔术3 分钟前
无虚拟dom怎么又流行起来了?
前端·javascript·vue.js
胡gh3 分钟前
如何聊懒加载,只说个懒可不行
前端·react.js·面试
Double__King6 分钟前
巧用 CSS 伪元素,让背景图自适应保持比例
前端
Mapmost8 分钟前
【BIM+GIS】BIM数据格式解析&与数字孪生适配的关键挑战
前端·vue.js·three.js
一涯9 分钟前
写一个Chrome插件
前端·chrome
鹧鸪yy16 分钟前
认识Node.js及其与 Nginx 前端项目区别
前端·nginx·node.js
跟橙姐学代码16 分钟前
学Python必须迈过的一道坎:类和对象到底是什么鬼?
前端·python
汪子熙18 分钟前
浏览器里出现 .angular/cache/19.2.6/abap_test/vite/deps 路径究竟说明了什么
前端·javascript·面试