前言

在阅读一些成熟项目、开源库或者打包后的代码时,我们偶尔会看到这样的写法:

void someAsyncTask();

或者:

value === void 0

再或者老代码里的:

<a href="javascript:void(0)">点击</a>

第一次看到时很容易困惑:void 是什么?它是在取消执行吗?为什么调用异步函数前面要加一个 voidvoid main() 又是什么意思?

本文会从日常开发最常见的场景讲起,解释 void 的基本语义、为什么它常被用来忽略 Promise,以及一些历史代码中常见的写法。看完之后,你至少能做到两件事:

  • 读懂别人代码里的 void
  • 知道自己什么时候该用,什么时候不该用。

日常开发中的 void:从语言特性到业界约定

void 是 JavaScript 里的一个一元运算符。

它的作用可以概括成一句话:

执行后面的表达式,但把整个表达式的结果变成 undefined

例如:

const result = void console.log("hello");

console.log(result); // undefined

这段代码里有两个重点:

  • console.log("hello") 仍然会执行;
  • void console.log("hello") 的最终结果是 undefined

所以,void 不是“不执行”,而是:

我需要执行这个表达式,但我不需要它的返回值。

在现代前端和 TypeScript 项目里,void 最常见的用途不是 void 0,也不是 javascript:void(0),而是用来表达:

void someAsyncTask();

它的意思是:

我知道 someAsyncTask() 返回一个 Promise,但这里我故意不 await 它。

比如:

function onClick() {
  void sendAnalyticsEvent("button_click");
}

这通常表示:点击按钮时发送埋点,但当前点击逻辑不需要等待埋点接口完成。

因此,现代业务代码里的 void 更多是一种约定俗成的表达方式:

  • 告诉读者:这里的 Promise 是故意不等待的;
  • 告诉 lint 工具:这不是忘记写 await
  • 告诉维护者:这个调用主要是为了触发副作用,而不是使用返回值。

不过要注意:

void 只是不关心返回值,不代表错误会被自动处理。

如果这个异步任务可能失败,并且失败需要被处理,就应该配合 .catch()

void sendAnalyticsEvent("button_click").catch((error) => {
  console.error("send analytics failed", error);
});

void 的基本语义

void expression 会做两件事:

  1. 执行 expression
  2. 返回 undefined

例如:

void 123;
// undefined

void "hello";
// undefined

void (() => 1)();
// undefined

重点是:表达式仍然会被执行,只是它的结果会被丢弃。

let count = 0;

const result = void count++;

console.log(count); // 1
console.log(result); // undefined

所以,void 不是“不执行”,而是“执行,但不要结果”。


最常见的现代用法:显式忽略 Promise

现代 TypeScript 项目里,最常见的 void 用法是:

void someAsyncTask();

它表达的是:

我知道这个函数返回 Promise,但这里我故意不等待它。

例如:

function onClick() {
  void sendAnalyticsEvent("button_click");
}

这里 sendAnalyticsEvent 可能是一个异步函数:

async function sendAnalyticsEvent(name: string) {
  await fetch("/api/analytics", {
    method: "POST",
    body: JSON.stringify({ name }),
  });
}

点击按钮时发送埋点,但页面逻辑不需要等待埋点请求完成。此时使用 void 可以清楚表达:

  • 这是一个异步操作;
  • 当前代码不关心它的返回值;
  • 不希望它阻塞主流程;
  • 这里不是忘记写 await

不过仍然要强调:如果这个 Promise reject 了,void 不会帮你处理错误。

如果你完全不关心错误,可以写:

void sendAnalyticsEvent("button_click");

如果你不等待结果,但想避免未处理的 Promise 错误,可以写:

void sendAnalyticsEvent("button_click").catch((error) => {
  console.error("send analytics failed", error);
});

配合 ESLint 的 no-floating-promises

很多 TypeScript 项目会启用类似规则:

@typescript-eslint/no-floating-promises

这个规则会提醒你:不要让 Promise 无处理地“悬空”。

例如:

async function refreshData() {
  // ...
}

function onChanged() {
  refreshData();
}

在严格项目里,这可能会被认为有风险,因为 refreshData() 返回了 Promise,但你既没有 await,也没有 .catch()

更明确的写法是:

function onChanged() {
  void refreshData();
}

这相当于告诉工具和维护者:

这个 Promise 是我有意不等待的。

不过需要注意,void refreshData() 能不能通过 ESLint,取决于项目里的规则配置。比如 @typescript-eslint/no-floating-promises 通常可以通过 ignoreVoid 之类的配置允许这种写法。

如果你需要等待它完成,应该写:

async function onChanged() {
  await refreshData();
}

如果你不等待,但需要处理错误,可以写:

function onChanged() {
  void refreshData().catch((error) => {
    console.error("refresh failed", error);
  });
}

回调函数期望返回 void

有些 API 的回调类型明确要求返回 void

例如:

type Listener = () => void;

function subscribe(listener: Listener) {
  // ...
}

如果传入:

subscribe(() => fetchLatestData());

这里箭头函数会隐式返回 fetchLatestData() 的结果。如果 fetchLatestData() 返回 Promise,那么这个回调从代码行为上看,其实返回了一个 Promise。

更清晰的写法是:

subscribe(() => void fetchLatestData());

或者:

subscribe(() => {
  void fetchLatestData();
});

这样语义更准确:

  • subscribe 需要的是一个普通回调;
  • fetchLatestData() 只是被触发;
  • 回调本身不把 Promise 返回给调用方。

这在 UI 事件、订阅回调、生命周期回调中都很常见。


使用 void 触发 IIFE

早期 JavaScript 中,立即执行函数表达式通常写成:

(function () {
  console.log("IIFE");
})();

也有人会写成:

void function () {
  console.log("IIFE");
}();

这里的 void 有两个作用:

  • function 出现在表达式位置;
  • 执行这个函数,但忽略它的返回值。

不过现代项目里,这种写法已经很少见。现在更推荐普通的 IIFE 写法:

(function () {
  console.log("IIFE");
})();

如果是异步入口,更常见的是:

async function main() {
  await startApp();
}

void main();

或者更严谨一点:

async function main() {
  await startApp();
}

void main().catch((error) => {
  console.error(error);
});

程序入口中的 void main()

在 Node.js 脚本、CLI 工具、构建脚本里,经常会看到这样的代码:

async function main() {
  await runBuild();
  await generateFiles();
}

void main();

这段代码对新手来说最容易误解。

这里的 void main() 不是在声明一个“返回 void 的 main 函数”。

它其实可以拆成两部分看:

main();

和:

void 某个表达式;

所以:

void main();

真正的意思是:

调用 main(),但不要使用 main() 的返回值。

为什么 main() 会有返回值呢?

因为 main 是一个 async function

async function main() {
  await runBuild();
  await generateFiles();
}

在 JavaScript 里,所有 async function 调用后都会返回一个 Promise。

也就是说:

const result = main();

这里的 result 不是 runBuild() 的结果,也不是 generateFiles() 的结果,而是一个 Promise:

const result: Promise<void> = main();

所以这段代码:

void main();

等价于在表达:

const promise = main();
// 我知道这里有一个 Promise,但我不打算在这里 await 它。

只是它不会真的创建一个变量,而是直接用 void 表达“我不要这个返回值”。


为什么不直接写 main()

你当然可以写:

main();

在很多情况下它也能正常运行。

但是在开启严格 ESLint 的项目里,工具可能会提醒你:

这里有一个 Promise 没有被 await,也没有被 catch。

也就是前面提到的 “floating promise”。

所以:

void main();

比:

main();

多表达了一层意思:

我知道 main() 返回 Promise,这里是故意不 await。

这对读代码的人和 lint 工具都更友好。


void main() 会等待 main 执行完吗?

不会。

void 不会等待 Promise 完成。

这点很重要。

async function main() {
  await runBuild();
}

void main();

console.log("after main");

这里的执行顺序可以理解为:

  1. 调用 main()
  2. main() 返回一个 Promise;
  3. void 丢弃这个 Promise;
  4. 继续执行后面的代码。

所以 void main() 只是启动了异步流程,不代表当前代码会停下来等它完成。

如果你在支持顶层 await 的环境里,并且确实想等待它完成,可以写:

await main();

但很多 Node.js 脚本、CLI 工具、CommonJS 文件或者较老的构建环境里,不一定方便直接使用顶层 await,所以会使用 main() 包一层异步入口。


那错误怎么办?

这是 void main() 最需要注意的地方。

如果 main() 里面报错了:

async function main() {
  await runBuild();
  await generateFiles();
}

void main();

void 不会帮你捕获错误。

更严谨的程序入口通常会这样写:

async function main() {
  await runBuild();
  await generateFiles();
}

void main().catch((error) => {
  console.error(error);
  process.exit(1);
});

这段代码的意思是:

  • 启动 main()
  • 不在顶层 await 它;
  • 但如果 main() 执行失败,要打印错误;
  • CLI 或脚本失败时,用 process.exit(1) 告诉系统“这个命令执行失败了”。

对于脚本、构建工具、CLI 来说,这种写法比裸写 void main() 更稳妥。


main()void main()await main() 有什么区别?

可以简单理解成下面这样:

写法含义是否等待完成是否表达“故意忽略 Promise”是否处理错误
main()调用 main,但不处理返回的 Promise不明显
void main()调用 main,并明确丢弃返回的 Promise
await main()调用 main,并等待它完成取决于外层 try/catch
void main().catch(handleError)调用 main,不等待,但处理错误

因此,程序入口里更推荐写成:

async function main() {
  await runBuild();
  await generateFiles();
}

void main().catch((error) => {
  console.error(error);
  process.exit(1);
});

如果是浏览器应用入口,也可以类似:

async function main() {
  await startApp();
}

void main().catch((error) => {
  console.error("app start failed", error);
});

简单说:

void main() 的重点不是“main 特殊”,而是“main() 返回了 Promise,但这里故意不 await”。

推荐实践

可以按照下面的规则判断是否使用 void

场景推荐写法
需要等待 Promise 完成await task()
不等待,但要处理错误void task().catch(handleError)
不等待,也不关心错误void task()
判断 undefinedvalue === undefined
阅读老代码或压缩产物可能看到 void 0
链接不跳转优先使用 <button> 或阻止默认行为

简单说:

现代业务代码里,void 最推荐的用法就是明确忽略 Promise。

总结

void 是一个简单但有历史包袱的 JavaScript 运算符。

它的核心语义是:

void expression;

意思是:

执行 expression,但让整个表达式的结果变成 undefined

在现代开发中,它最有价值的地方是:

void somePromiseReturningFunction();

这是一种常见约定,用来表达:

  • 我知道这是 Promise;
  • 我故意不等待它;
  • 我只需要它的副作用;
  • 当前回调应该返回 void
  • 这不是忘记写 await

但也要记住:

void 不会处理 Promise 错误,也不会让异步任务变安全。

如果错误需要被处理,应该写:

void task().catch(handleError);

而其他用法,比如 void 0javascript:void(0)void function () {}(),更多是历史代码、老项目、压缩产物或者特殊技巧中的写法。理解它们有助于读代码,但日常新业务代码里不一定推荐使用。

最终建议是:

在需要明确忽略 Promise 时使用 void;其他场景优先选择更直观、更现代的写法。
分类: JavaScript 标签: void异步等待undefined

评论

暂无评论数据

暂无评论数据

目录