好用的 TypeScript 新特性:using 关键字介绍

TypeScript 5.2 版本中新添加了 using 关键字,目前该关键字的提案也进入了 ECMAScript 的 Stage 3,也就是说很快就会进入 JavaScript 语言本身中。using 和我们非常熟悉的 const, letvar 一样都是用于变量声明的,那么它到底有什么与众不同的地方呢?

使用场景

假设我们现在写了一个打印日志到文件中的函数:

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]() 函数,因为 cd 被放在块级作用域中,所以比外层的 a, be 更快被释放。

在处理这类需要释放连接的功能时,最怕的就是中间的某一步代码停止了整个函数的运行,比如说中途 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 的原生支持也指日可待了。

相关推荐
万物得其道者成1 分钟前
React Zustand状态管理库的使用
开发语言·javascript·ecmascript
小白小白从不日白2 分钟前
react hooks--useReducer
前端·javascript·react.js
下雪天的夏风14 分钟前
TS - tsconfig.json 和 tsconfig.node.json 的关系,如何在TS 中使用 JS 不报错
前端·javascript·typescript
diygwcom26 分钟前
electron-updater实现electron全量版本更新
前端·javascript·electron
volodyan29 分钟前
electron react离线使用monaco-editor
javascript·react.js·electron
^^为欢几何^^37 分钟前
lodash中_.difference如何过滤数组
javascript·数据结构·算法
Hello-Mr.Wang42 分钟前
vue3中开发引导页的方法
开发语言·前端·javascript
程序员凡尘1 小时前
完美解决 Array 方法 (map/filter/reduce) 不按预期工作 的正确解决方法,亲测有效!!!
前端·javascript·vue.js
编程零零七4 小时前
Python数据分析工具(三):pymssql的用法
开发语言·前端·数据库·python·oracle·数据分析·pymssql
北岛寒沫5 小时前
JavaScript(JS)学习笔记 1(简单介绍 注释和输入输出语句 变量 数据类型 运算符 流程控制 数组)
javascript·笔记·学习