日常开发中的 void:从忽略 Promise 到理解 JavaScript 历史写法
前言
在阅读一些成熟项目、开源库或者打包后的代码时,我们偶尔会看到这样的写法:
void someAsyncTask();或者:
value === void 0再或者老代码里的:
<a href="javascript:void(0)">点击</a>第一次看到时很容易困惑:void 是什么?它是在取消执行吗?为什么调用异步函数前面要加一个 void?void 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 会做两件事:
- 执行
expression; - 返回
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");这里的执行顺序可以理解为:
- 调用
main(); main()返回一个 Promise;void丢弃这个 Promise;- 继续执行后面的代码。
所以 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() |
| 判断 undefined | value === undefined |
| 阅读老代码或压缩产物 | 可能看到 void 0 |
| 链接不跳转 | 优先使用 <button> 或阻止默认行为 |
简单说:
现代业务代码里,void 最推荐的用法就是明确忽略 Promise。总结
void 是一个简单但有历史包袱的 JavaScript 运算符。
它的核心语义是:
void expression;意思是:
执行expression,但让整个表达式的结果变成undefined。
在现代开发中,它最有价值的地方是:
void somePromiseReturningFunction();这是一种常见约定,用来表达:
- 我知道这是 Promise;
- 我故意不等待它;
- 我只需要它的副作用;
- 当前回调应该返回
void; - 这不是忘记写
await。
但也要记住:
void 不会处理 Promise 错误,也不会让异步任务变安全。如果错误需要被处理,应该写:
void task().catch(handleError);而其他用法,比如 void 0、javascript:void(0)、void function () {}(),更多是历史代码、老项目、压缩产物或者特殊技巧中的写法。理解它们有助于读代码,但日常新业务代码里不一定推荐使用。
最终建议是:
在需要明确忽略 Promise 时使用 void;其他场景优先选择更直观、更现代的写法。 本文系作者 @木灵鱼儿 原创发布在木灵鱼儿站点。未经许可,禁止转载。
暂无评论数据