TypeScript 5.2 版本中新添加了 using
关键字,目前该关键字的提案也进入了 ECMAScript 的 Stage 3,也就是说很快就会进入 JavaScript 语言本身中。using
和我们非常熟悉的 const
, let
和 var
一样都是用于变量声明的,那么它到底有什么与众不同的地方呢?
使用场景
假设我们现在写了一个打印日志到文件中的函数:
ts
function log(content: string) {
const file = fs.openSync("log.txt", "w+");
file.writeFileSync(file, content);
// 释放文件
fs.closeSync(file);
}
非常简单的一个功能,但过了几天,组长要求我们把日志区分为普通日志和警告日志,此时我们需要再改造一下这个函数:
ts
function log(content: string, isWarn?: boolean) {
const file = fs.openSync("log.txt", "w+");
file.writeFileSync(file, content);
if (!isWarn) {
return;
}
// isWarn 的日志添加警告标识
file.writeFileSync(file, "[warn]");
// 释放文件
fs.closeSync(file);
}
这个时候运行函数会出现问题,因为在 isWarn
为 false 的情况下,我们直接 return 了,没有走到函数末尾 fs.closeSync()
,这会导致文件连接没有释放。
虽然说上面这段代码的写法本身就有些问题,但在复杂的逻辑中确实很容易出现忘记释放文件、数据库连接这样的现象,而 using
关键字就能够很好地解决这类问题。
把上面的例子用 using
改写如下所示:
ts
function log(content: string, isWarn?: boolean) {
using file = fs.openSync("log.txt", "w+");
file.writeSync(file, content);
if (!isWarn) {
return;
}
// isWarn 的日志添加警告标识
file.writeSync(file, "[warn]");
}
可以看到我们甚至不需要在函数末尾调用 fs.closeSync()
,这是因为 using
关键字声明的变量会在被释放时执行一些额外的操作,有点类似于 C++ 中的析构函数。在这个例子中我们 using 的是 node.js 原生的 openSync
函数返回的变量,它会在变量被删除时自动释放文件的连接,这样我们就不需要自己手动进行管理了,减少了犯错误的可能。
使用方法
上面的例子中,我们不需要任何额外操作就获得了自动释放文件的能力,这是因为标准库已经帮我们定义好了文件变量被释放时的行为。如果是我们自己写的功能,则需要自己对变量释放时要做的操作进行定义。
假设我们自己写了一个打开文件的函数:
ts
function openFile(path) {
console.log("打开文件:", path);
const file = fs.openSync(path, "w+");
return {
handle: file,
[Symbol.dispose]() {
console.log("释放文件:", path);
fs.closeSync();
},
};
}
返回对象中的 [Symbol.dispose]()
会在变量被回收时调用(如果变量是用 using
声明的),这样我们就不需要让用户手动释放文件了,保证了程序的正确性。
我们来看看使用这个函数的实例:
ts
using a = openFile("a")
fs.writeSync(file, "hello");
运行该代码会打印:
css
打开文件:a
释放文件:a
当我们调用 openFile()
创建变量 a
时,我们会打印 打开文件:a
并把文件的描述符返回。当程序运行结束时,变量 a
的生命周期结束,[Symbol.dispose]()
被调用,打印 释放文件:a
并关闭文件连接。
我们再看一个复杂点的例子:
ts
using a = openFile("a")
using b = openFile("b")
{
using c = openFile("c")
using d = openFile("d")
}
using e = openFile("e")
大家可以停下来想一想,这段程序会打印出什么样的内容?
答案是:
css
打开文件:a
打开文件:b
打开文件:c
打开文件:d
释放文件:d
释放文件:c
打开文件:e
释放文件:b
释放文件:a
可以看到,每个变量在其生命周期结束时就调用了它的 [Symbol.dispose]()
函数,因为 c
和 d
被放在块级作用域中,所以比外层的 a
, b
和 e
更快被释放。
在处理这类需要释放连接的功能时,最怕的就是中间的某一步代码停止了整个函数的运行,比如说中途 throw 了一个错误,然后释放连接的代码就被跳过了。这种情况下 using
关键字也能够很好的完成任务。以下面的代码为例:
ts
using a = openFile("a")
using b = openFile("b")
{
using c = openFile("c")
throw new Error()
using d = openFile("d")
}
using e = openFile("e")
此时控制台打印如下:
css
打开文件:a
打开文件:b
打开文件:c
释放文件:c
释放文件:b
释放文件:a
可以看到抛出错误时,已经被打开的几个文件也正确地关闭了,这就是 using
关键字的功劳。
异步 using
我们刚刚处理文件用的是同步方法,但这并不是最佳实践,通常我们会使用异步的方法来进行文件的操作。但是 [Symbol.dispose]()
必须是同步的函数,这个时候我们就要用到另外一个 Symbol,它的名字就是......没错,[Symbol.asyncDispose]()
!
我们用异步的方式改写一下刚刚的 openFile()
函数:
ts
function openFile(path) {
console.log("打开文件:", path);
const file = fs.openSync(path, "w+");
return {
handle: file,
async [Symbol.asyncDispose]() {
console.log("释放文件:", path);
await fs.close();
},
};
}
这个时候我们需要改变一下使用 using
关键字的方式,在 using
之前添加 await
:
ts
async (() => {
await using a = openFile("a")
await using b = openFile("b")
})()
这里的 await
关键字具有一定的迷惑性,看似是变量声明语句被 await
了,实际上并没有。我们加一行 log 试试:
ts
async (() => {
await using a = openFile("a")
console.log("haha")
await using b = openFile("b")
})()
输出:
css
打开文件:a
haha
打开文件:b
释放文件:b
释放文件:a
可以发现 openFile()
和 console.log()
是同步执行的。那么异步的是什么呢?当然就是 [Symbol.asyncDispose]()
函数了,但因为它是在函数最后隐式执行的,所以我们感觉不到。
总结
using
是一个新的变量声明关键字,它能够帮助我们更好地管理需要释放的资源,减少代码出错的概率,是非常实用的一个新特性。目前该特性已经得到 TypeScript 新版本的支持,JavaScript 的原生支持也指日可待了。