using
using
声明声明了块级作用域的局部变量,这些变量是同步处置的。与 const
类似,用 using
声明的变量必须被初始化且不能被重新赋值。变量的值必须是 null
、undefined
,或者是一个带有 [Symbol.dispose]()
方法的对象。当变量超出作用域时,对象的 [Symbol.dispose]()
方法会被调用,以确保资源被释放。
语法
using name1 = value1;
using name1 = value1, name2 = value2;
using name1 = value1, name2 = value2, /* …, */ nameN = valueN;
描述
此声明可用于
最值得注意的是,它不能用于
using
声明一个一次性资源,该资源与变量作用域(块、函数、模块等)的生命周期绑定。当作用域退出时,资源会同步被处置。变量可以为 null
或 undefined
,因此资源可以是可选的。
当变量首次声明且其值为非空(non-nullish)时,会从对象中获取一个处置器。如果 [Symbol.dispose]
属性不包含函数,则会抛出 TypeError
。此处置器会保存到作用域中。
当变量超出作用域时,处置器会被调用。如果作用域包含多个 using
或 await using
声明,所有处置器将按声明的逆序运行,无论声明类型如何。所有处置器都保证会运行(很像 try...catch...finally
中的 finally
块)。所有在处置过程中抛出的错误,包括导致作用域退出的初始错误(如果适用),都会被聚合到一个 SuppressedError
中,其中较早的异常作为 suppressed
属性,较晚的异常作为 error
属性。此 SuppressedError
会在处置完成后抛出。
using
将资源管理与词法作用域绑定,这既方便有时也令人困惑。有许多方法可以在变量本身超出作用域时保留变量的值,因此你可能会持有对已处置资源的引用。有关可能不按预期行为的示例,请参阅下文。如果你想手动管理资源处置,同时保持相同的错误处理保证,可以使用 DisposableStack
代替。
示例
在以下示例中,我们假设有一个简单的 Resource
类,它具有 getValue
方法和 [Symbol.dispose]()
方法
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
声明的资源在退出块时被处置。
{
using resource = new Resource();
console.log(resource.getValue());
// resource disposed here
}
函数中的 using
你可以在函数体中使用 using
。在这种情况下,资源在函数执行完毕时,紧接在函数返回之前被处置。
function example() {
using resource = new Resource();
return resource.getValue();
}
在这里,resource[Symbol.dispose]()
将在 getValue()
之后,在 return
语句执行之前被调用。
在资源被 闭包 捕获的情况下,它可能会比声明存活更久
function example() {
using resource = new Resource();
return () => resource.getValue();
}
在这种情况下,如果你调用 example()()
,你将始终在一个已经处置的资源上执行 getValue
,因为资源在 example
返回时已被处置。如果你想在回调被调用一次后立即处置资源,请考虑这种模式
function example() {
const resource = new Resource();
return () => {
using resource2 = resource;
return resource2.getValue();
};
}
在这里,我们将用 const
声明的资源别名为一个用 using
声明的资源,这样资源只在回调被调用后才会被处置;请注意,如果回调从未被调用,则资源将永远不会被清理。
模块中的 using
你可以在模块的顶层使用 using
。在这种情况下,资源在模块执行完毕时被处置。
using resource = new Resource();
export const value = resource.getValue();
// resource disposed here
export using
是无效语法,但你可以 export
一个在其他地方用 using
声明的变量
using resource = new Resource();
export { resource };
这仍然不被鼓励,因为导入者将始终收到已处置的资源。类似于闭包问题,这会导致资源的值比变量的生命周期更长。
带 for...of
的 using
你可以在 for...of
循环的初始化器中使用 using
。在这种情况下,资源在每次循环迭代时都会被处置。
const resources = [new Resource(), new Resource(), new Resource()];
for (using resource of resources) {
console.log(resource.getValue());
// resource disposed here
}
多个 using
以下是声明多个可处置资源的两种等效方式
using resource1 = new Resource(),
resource2 = new Resource();
// OR
using resource1 = new Resource();
using resource2 = new Resource();
在这两种情况下,当作用域退出时,resource2
在 resource1
之前被处置。这是因为 resource2
可能依赖于 resource1
,因此它首先被处置以确保在 resource2
被处置时 resource1
仍然可用。
可选的 using
using
允许变量为 null
或 undefined
,因此资源可以是可选的。这意味着你不需要这样做
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);
}
但可以这样做
using resource = acquireResource();
console.log(resource?.getValue());
不使用变量的 using
声明
你可以使用 using
实现自动资源处置,甚至不需要使用变量。这对于在块中设置上下文非常有用,例如创建锁
{
using _ = new Lock();
// Perform concurrent operations here
// Lock disposed (released) here
}
请注意,_
是一个普通标识符,但它约定俗成地用作“一次性”变量。要创建多个未使用变量,你需要使用不同的名称,例如使用以 _
为前缀的变量名。
初始化和暂时性死区
using
变量受制于与 let
和 const
变量相同的 暂时性死区 限制。这意味着你不能在初始化之前访问变量——资源的有效生命周期严格地从其初始化到其作用域的结束。这使得 RAII 风格的资源管理成为可能。
let useResource;
{
useResource = () => resource.getValue();
useResource(); // Error: Cannot access 'resource' before initialization
using resource = new Resource();
useResource(); // Valid
}
useResource(); // Error: Resource is disposed
错误处理
using
声明在存在错误的情况下管理资源处置最为有用。如果不小心,一些资源可能会因为错误阻止后续代码执行而泄漏。
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
,你可能会这样做
try {
const resource = new Resource();
handleResource(resource);
resource[Symbol.dispose]();
} catch (e) {
console.error(e);
}
但是,如果 handleResource()
抛出错误,那么控制流永远不会到达 resource[Symbol.dispose]()
,并且资源会泄漏。此外,如果你有两个资源,那么在早期处置中抛出的错误可能会阻止后期处置的运行,导致更多的泄漏。
考虑一个更复杂的情况,其中处置器本身抛出错误
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 |
浏览器兼容性
加载中…