using

可用性有限

此特性不是基线特性,因为它在一些最广泛使用的浏览器中不起作用。

using 声明声明了块级作用域的局部变量,这些变量是同步处置的。与 const 类似,用 using 声明的变量必须被初始化且不能被重新赋值。变量的值必须是 nullundefined,或者是一个带有 [Symbol.dispose]() 方法的对象。当变量超出作用域时,对象的 [Symbol.dispose]() 方法会被调用,以确保资源被释放。

语法

js
using name1 = value1;
using name1 = value1, name2 = value2;
using name1 = value1, name2 = value2, /* …, */ nameN = valueN;
nameN

要声明的变量名。每个变量名都必须是合法的 JavaScript 标识符,并且不能解构绑定模式

valueN

变量的初始值。它可以是任何合法的表达式,但其值必须是 nullundefined,或者是一个带有 [Symbol.dispose]() 方法的对象。

描述

此声明可用于

最值得注意的是,它不能用于

  • 脚本的顶层,因为脚本作用域是持久的。
  • switch 语句的顶层。
  • for...in 循环的初始化器中。因为循环变量只能是字符串或符号,所以这样做没有意义。

using 声明一个一次性资源,该资源与变量作用域(块、函数、模块等)的生命周期绑定。当作用域退出时,资源会同步被处置。变量可以为 nullundefined,因此资源可以是可选的。

当变量首次声明且其值为非空(non-nullish)时,会从对象中获取一个处置器。如果 [Symbol.dispose] 属性不包含函数,则会抛出 TypeError。此处置器会保存到作用域中。

当变量超出作用域时,处置器会被调用。如果作用域包含多个 usingawait using 声明,所有处置器将按声明的逆序运行,无论声明类型如何。所有处置器都保证会运行(很像 try...catch...finally 中的 finally 块)。所有在处置过程中抛出的错误,包括导致作用域退出的初始错误(如果适用),都会被聚合到一个 SuppressedError 中,其中较早的异常作为 suppressed 属性,较晚的异常作为 error 属性。此 SuppressedError 会在处置完成后抛出。

using 将资源管理与词法作用域绑定,这既方便有时也令人困惑。有许多方法可以在变量本身超出作用域时保留变量的值,因此你可能会持有对已处置资源的引用。有关可能不按预期行为的示例,请参阅下文。如果你想手动管理资源处置,同时保持相同的错误处理保证,可以使用 DisposableStack 代替。

示例

在以下示例中,我们假设有一个简单的 Resource 类,它具有 getValue 方法和 [Symbol.dispose]() 方法

js
class Resource {
  value = Math.random();
  #isDisposed = false;

  getValue() {
    if (this.#isDisposed) {
      throw new Error("Resource is disposed");
    }
    return this.value;
  }

  [Symbol.dispose]() {
    this.#isDisposed = true;
    console.log("Resource disposed");
  }
}

块中的 using

using 声明的资源在退出块时被处置。

js
{
  using resource = new Resource();
  console.log(resource.getValue());
  // resource disposed here
}

函数中的 using

你可以在函数体中使用 using。在这种情况下,资源在函数执行完毕时,紧接在函数返回之前被处置。

js
function example() {
  using resource = new Resource();
  return resource.getValue();
}

在这里,resource[Symbol.dispose]() 将在 getValue() 之后,在 return 语句执行之前被调用。

在资源被 闭包 捕获的情况下,它可能会比声明存活更久

js
function example() {
  using resource = new Resource();
  return () => resource.getValue();
}

在这种情况下,如果你调用 example()(),你将始终在一个已经处置的资源上执行 getValue,因为资源在 example 返回时已被处置。如果你想在回调被调用一次后立即处置资源,请考虑这种模式

js
function example() {
  const resource = new Resource();
  return () => {
    using resource2 = resource;
    return resource2.getValue();
  };
}

在这里,我们将用 const 声明的资源别名为一个用 using 声明的资源,这样资源只在回调被调用后才会被处置;请注意,如果回调从未被调用,则资源将永远不会被清理。

模块中的 using

你可以在模块的顶层使用 using。在这种情况下,资源在模块执行完毕时被处置。

js
using resource = new Resource();
export const value = resource.getValue();
// resource disposed here

export using 是无效语法,但你可以 export 一个在其他地方用 using 声明的变量

js
using resource = new Resource();
export { resource };

这仍然不被鼓励,因为导入者将始终收到已处置的资源。类似于闭包问题,这会导致资源的值比变量的生命周期更长。

for...ofusing

你可以在 for...of 循环的初始化器中使用 using。在这种情况下,资源在每次循环迭代时都会被处置。

js
const resources = [new Resource(), new Resource(), new Resource()];
for (using resource of resources) {
  console.log(resource.getValue());
  // resource disposed here
}

多个 using

以下是声明多个可处置资源的两种等效方式

js
using resource1 = new Resource(),
  resource2 = new Resource();

// OR

using resource1 = new Resource();
using resource2 = new Resource();

在这两种情况下,当作用域退出时,resource2resource1 之前被处置。这是因为 resource2 可能依赖于 resource1,因此它首先被处置以确保在 resource2 被处置时 resource1 仍然可用。

可选的 using

using 允许变量为 nullundefined,因此资源可以是可选的。这意味着你不需要这样做

js
function acquireResource() {
  // Imagine some real-world relevant condition here,
  // such as whether there's space to allocate for this resource
  if (Math.random() < 0.5) {
    return null;
  }
  return new Resource();
}

const maybeResource = acquireResource();

if (maybeResource) {
  using resource = maybeResource;
  console.log(resource.getValue());
} else {
  console.log(undefined);
}

但可以这样做

js
using resource = acquireResource();
console.log(resource?.getValue());

不使用变量的 using 声明

你可以使用 using 实现自动资源处置,甚至不需要使用变量。这对于在块中设置上下文非常有用,例如创建锁

js
{
  using _ = new Lock();
  // Perform concurrent operations here
  // Lock disposed (released) here
}

请注意,_ 是一个普通标识符,但它约定俗成地用作“一次性”变量。要创建多个未使用变量,你需要使用不同的名称,例如使用以 _ 为前缀的变量名。

初始化和暂时性死区

using 变量受制于与 letconst 变量相同的 暂时性死区 限制。这意味着你不能在初始化之前访问变量——资源的有效生命周期严格地从其初始化到其作用域的结束。这使得 RAII 风格的资源管理成为可能。

js
let useResource;
{
  useResource = () => resource.getValue();
  useResource(); // Error: Cannot access 'resource' before initialization
  using resource = new Resource();
  useResource(); // Valid
}
useResource(); // Error: Resource is disposed

错误处理

using 声明在存在错误的情况下管理资源处置最为有用。如果不小心,一些资源可能会因为错误阻止后续代码执行而泄漏。

js
function handleResource(resource) {
  if (resource.getValue() > 0.5) {
    throw new Error("Resource value too high");
  }
}

try {
  using resource = new Resource();
  handleResource(resource);
} catch (e) {
  console.error(e);
}

这将成功捕获 handleResource 抛出的错误并记录它,无论 handleResource 是否抛出错误,资源都会在退出 try 块之前被处置。

在这里,如果你不使用 using,你可能会这样做

js
try {
  const resource = new Resource();
  handleResource(resource);
  resource[Symbol.dispose]();
} catch (e) {
  console.error(e);
}

但是,如果 handleResource() 抛出错误,那么控制流永远不会到达 resource[Symbol.dispose](),并且资源会泄漏。此外,如果你有两个资源,那么在早期处置中抛出的错误可能会阻止后期处置的运行,导致更多的泄漏。

考虑一个更复杂的情况,其中处置器本身抛出错误

js
class CantDisposeMe {
  #name;
  constructor(name) {
    this.#name = name;
  }
  [Symbol.dispose]() {
    throw new Error(`Can't dispose ${this.#name}`);
  }
}

let error;

try {
  using resource1 = new CantDisposeMe("resource1");
  using resource2 = new CantDisposeMe("resource2");
  throw new Error("Error in main block");
} catch (e) {
  error = e;
}

你可以在浏览器的控制台中检查抛出的错误。它具有以下结构

SuppressedError: An error was suppressed during disposal
  suppressed: SuppressedError: An error was suppressed during disposal
    suppressed: Error: Can't dispose resource1
    error: Error: Error in main block
  error: Error: Can't dispose resource2

如你所见,error 包含所有在处置过程中抛出的错误,作为一个 SuppressedError。每个附加错误都作为 error 属性添加,原始错误作为 suppressed 属性添加。

规范

规范
ECMAScript 异步显式资源管理
# prod-UsingDeclaration

浏览器兼容性

另见