原问题:
有个疑惑,为什么在 JavaScript 里,如果在某个函数里使用了
await
语法,则这个函数必须定义成async
的? 如下代码:// 函数 a 的定义 async function a(){ await b() } await a(); // 函数 a 的调用
从逻辑上来讲,如果函数
a
不定义成async
,执行器在调用a
的时候通过是否加await
来区别,好像也可以达到同样的效果。
以下是回答备份:
首先 async
/await
和 Generator 的思路是一脉相承的 —— async
/await
提案最早的草稿里就说了,它相当于一个带自动 spawn
功能的 Generator,就是一个语法糖。1
所以,它带上特殊标记(async
)的原因应该和 Generator 需要用 *
标记的原因类似。
然后我们需要理解为什么 Generator 不能直接用 function
而是用了一个 function *
的怪异语法。
追查 Generator 语法的最早版本实现我们可以发现,Mozilla JS 1.7 里的 Generator 是不需要特殊标记的,它就是一个包含了 yield
语句的特殊函数。
但是 Mozilla 的人计划把这个语法特性标准化时有了不同的考虑(以下原因排名分先后):
yield
只有在严格模式下才是一个保留字,在非严格模式下它可以被当作普通的变量名使用,这样给解析造成了困难。2 而标准委员会的人不希望限制新特性的使用场景,所以他们必须选择一种向后兼容的语法。 3- 从可读性角度考虑,如果函数体很长,而
yield
出现在很后面,读者需要很仔细地阅读才能意识到这个不是普通函数,不利于快速理解代码。async
/await
同理:await
在非严格模式(sloppy mode)下不是保留字4,以及可读性考虑。
现在随着现代前端代码逐渐迁移到了 ES Module(默认是严格模式),标准委员会也在计划扩大 await
的使用场景,所以有了 top-level await
提案 。在这个提案里,只要是在 Module 环境下,我们是可以直接写 await
的:
// x.mjs
console.log("X1");
await new Promise(r => setTimeout(r, 1000));
console.log("X2");
但是在这种场景使用 await
语法,需要考虑的不仅仅在于词法解析、语法设计等等,而是有很多实现细节上的小坑:比如潜在的死锁问题、可能模块加载时间受影响、和 CommonJS 模块的兼容等等……
所以当初标准委员会才决定把 top-level await
从 await
/async
提案中摘出来,单独标准化,这过程中还涉及到和各个执行环境(loader)的协作。后续标准讨论过程中还引发了一次比较大的讨论:Top-level await
is a footgun。现在其中大部分问题都有了一定共识,提案也进展到了 Stage 3,非常建议读一下提案的 FAQ 部分。不过最后进入标准还是有困难,作为 TC39 成员代表的贺老已经表达了强烈反对(参见此答案的评论部分)……
从这个提案的经历,我们也可以得出本问题的另一个解答:相比在任意情况下都允许 await
,把它限制在 async function {}
里,实现起来实在是简单得多了。
出于实用主义的考虑,先把限制加上去,功能实现出来,应该更符合大多数 JavaScript 开发者的利益吧。
Footnotes
-
最初设想里,
async function <name>?<argumentlist><body>
等价于function <name>?<argumentlist>{ return spawn(function*() <body>); }
↩ -
以下代码在非严格模式下是合法的:
function f() { var yield = 1; return yield; }
↩ -
一个当时很多人持有的观点:ES6 doesn’t need opt-in。
现在回过头来看,"use strict"
和script type="module"
这种形式的 opt-in 确实很影响用户接受度(要不是 babel / webpack 的流行,这两种特性使用情况肯定比现在惨淡得多),所以标准委员会整体倾向于 ES6 新特性尽可能兼容旧代码。 ↩ -
最最早版本的
async function
草案是这个:[[strawman:deferred_functions]],其中提到了语法上的考虑。 ↩