想象你走进一家餐厅,点了一份招牌菜。服务员没有让你站在柜台前干等,而是递给你一张取号单:“您先回座位,菜好了我叫您。”这张取号单,就是 JavaScript 里的 Promise。它在手里时是“等待中”,菜端上桌是“成功”,厨房告诉你做不了是“失败”。你可以在这张单子上挂几个叮嘱:“菜好了叫我”“失败了也通知我”“不管怎样吃完再买单”——这就是 Promise 的链式能力。后来餐厅升级了服务,你可以直接“在座位上等上菜”,代码像看菜单一样从上往下读——这就是 async/await。本文用这个“餐厅点餐”的比喻,带你彻底搞懂异步编程的现代套路。

岗位关联

对 AI 从业者:无论做前端可视化、模型接口调用还是数据管道,异步编程都是让 AI 应用“不卡顿”的基石。掌握 Promise 与 async/await,你写出的代码会更像一条流畅的服务流水线,而不是一团乱麻。

目录
  1. 可视化演示
  2. 三种状态
  3. 状态流转
  4. 链式调用
  5. 语法糖原理
  6. 错误处理
  7. 并行串行
  8. 回调地狱
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示Promise三种状态流转、then/catch/finally链式调用机制,以及Promise.all并行与await串行执行的时间对比。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Promise三种状态流转 Pending:初始状态,等待异步操作完成 Fulfilled:操作成功,状态不可逆 Pending → Fulfilled | Rejected 状态一旦确定不可更改,这是Promise可靠性的基础

三种状态

当你拿到取号单时,它只是一张薄薄的纸,上面没有菜,只有一排号码。这张单子此刻处于“等待中”——厨房还在忙活。过了一会儿,服务员端着热腾腾的菜走来,单子的使命完成了,它变成“成功”;如果厨师告诉你“今天的鱼卖完了”,单子就变成“失败”。这就是 Promise 的三种状态:pending、fulfilled、rejected。

在 JavaScript 中,一个 Promise 对象从创建那一刻起,就必然处于这三种状态之一。你通过 new Promise((resolve, reject) => {...}) 创建它,executor 中的异步任务完成时调用 resolve(value),失败时调用 reject(reason)。这三个状态不是装饰,而是异步结果的形式化表达:pending 表示结果未知;fulfilled 表示已成功拿到结果,附带一个值 value;rejected 表示失败,附带一个原因 reason。

特别强调:状态一旦从 pending 变成 fulfilled 或 rejected,就永久锁定,后续再调用 resolve 或 reject 都会被忽略。这就像取号单一旦变成“成功”,服务员绝不会再把它改成“失败”——菜都上桌了,怎么可能又撤回?所以 Promise 的状态不可逆,这是它可靠性的基石。

回到餐厅场景,你手中的取号单永远只会命中一种结局:要么等来菜,要么等来“卖完了”,没有第三种可能。理解这一点,你就抓住了 Promise 的核心。

Pending→resolve(value)Fulfilled,Pending→reject(reason)Rejected\displaystyle \text{Pending} \xrightarrow{\text{resolve(value)}} \text{Fulfilled},\quad \text{Pending} \xrightarrow{\text{reject(reason)}} \text{Rejected}
这个公式表示 Promise 只有两条合法的状态转移路径,而且方向不可逆。
  1. 等待态起始\text{Pending} 表示 Promise 刚创建时的等待状态,相当于取号单刚打印出来,厨房还没出菜。
    Pending\displaystyle \text{Pending}
  2. 成功转移箭头 \xrightarrow{\text{resolve(value)}} 表示调用 resolve 并携带一个值 value,状态转向 Fulfilled。
    →resolve(value)\displaystyle \xrightarrow{\text{resolve(value)}}
  3. 失败转移\text{reject(reason)} 表示调用 reject 并携带失败原因 reason,状态转向 Rejected。
    →reject(reason)\displaystyle \xrightarrow{\text{reject(reason)}}
  4. 终点状态\text{Fulfilled} 与 \text{Rejected} 是两个吸收态,一旦到达就固定,任何后续操作都无法改变。
    Fulfilled,Rejected\displaystyle \text{Fulfilled}, \text{Rejected}

状态流转

取号单不会自己变成“成功”或“失败”,是后厨的动作决定了它的命运。厨师做好菜,喊一声“32号!”,单子就变成了“成功”;厨师发现食材不够,在单子上画个叉,它就变成了“失败”。在 Promise 的世界里,这两个“喊话”就是 resolve 和 reject。

resolve 和 reject 是 executor 中两个函数参数。resolve(value) 将 Promise 推向 fulfilled,且把 value 传递给后续的 then 回调;reject(reason) 将 Promise 推向 rejected,并把 reason 传递给后续的 catch 回调。一旦调用其中一个,Promise 的状态就立刻锁定,另一个调用会被忽略,这就是“一次性转变”。

这里有个容易忽略的细节:value 和 reason 可以是任何 JavaScript 值。value 常是接口返回的数据,reason 常是 Error 对象。它们被“存”在 Promise 内部,等待消费者(then/catch)来取。你可以把 Promise 想象成一个带锁的盒子,resolve 存进成功结果,reject 存进失败原因,锁上之后只有对应的钥匙能打开。

你手中那张取号单,最终只能被服务员喊一次号,或者被划掉一次。Promise 的 resolve/reject 也遵循同样的规则——先到先得,后到无效。

state(Promise)={pending未决fulfilled(v)成功,携带值 vrejected(r)失败,携带原因 r\displaystyle \text{state}(\text{Promise}) = \begin{cases} \text{pending} & \text{未决} \\ \text{fulfilled}(v) & \text{成功,携带值 } v \\ \text{rejected}(r) & \text{失败,携带原因 } r \end{cases}
这是 Promise 状态的数学缩影:pending 没有附带信息,而 fulfilled 和 rejected 分别附带值和原因。
  1. 状态函数\text{state}(\text{Promise}) 表示我们观察一个 Promise 当前处于什么状态,就像看取号单上的标记。
    state(Promise)\displaystyle \text{state}(\text{Promise})
  2. 成功分支\text{fulfilled}(v) 表示成功状态携带一个值 v,这个值会传给 then 中的回调。
    fulfilled(v)\displaystyle \text{fulfilled}(v)
  3. 失败分支\text{rejected}(r) 表示失败状态携带一个原因 r,这个原因会传给 catch 中的回调。
    rejected(r)\displaystyle \text{rejected}(r)
  4. 未决分支\text{pending} 表示还未分晓,相当于后厨还在忙,没有喊号也没有画叉。
    pending\displaystyle \text{pending}

链式调用

以前你想知道菜好了没,得守在窗口一遍遍问。现在取号单让你把“叮嘱”写成一串:先等菜好,菜好了加个米饭,米饭上了再结账。Promise 的 then/catch/finally 就是这种穿成串的叮嘱。

promise.then(onFulfilled, onRejected) 用于注册成功或失败的回调;catch(onRejected) 专门处理失败;finally(onFinally) 无论成功失败都会执行。关键特性是:then 和 catch 都会返回一个新的 Promise,所以可以无限链接下去。每个 then 中 return 的值,会成为下一个 then 收到的参数,这就是“返回值透传”。

比如 p.then(f).then(g),如果 f 返回一个新 Promise,g 会等待它完成后再执行。这就像你去餐厅:先等 A 菜上桌,再让服务员去催 B 菜,最后等 B 菜上桌再动筷子。每一步都清清楚楚,不会挤在一起。

链式调用让异步流程从“层层嵌套”变成“一条直线”。你只需要像写购物清单一样,把每一步要做的事写在 then 里,代码自然就流畅了。

p.then(f).then(g)≡p.then(x↦g(f(x)))\displaystyle p.\text{then}(f).\text{then}(g) \equiv p.\text{then}(x \mapsto g(f(x)))
这个公式说明连续 two 个 then 等价于把两个回调组合成一个新回调,从内向外依次执行。
  1. 起点 Promisep 表示初始的 Promise 对象,相当于第一张取号单。
    p\displaystyle p
  2. 第一次挂单p.\text{then}(f) 在 p 上注册回调 f,等待 p 的结果。
    p.then(f)\displaystyle p.\text{then}(f)
  3. 第二次挂单.then(g) 在前一个 then 返回的新 Promise 上再注册 g,形成串联。
    then(g)\displaystyle \text{then}(g)
  4. 等价组合x \mapsto g(f(x)) 表示把 f 和 g 组合成一个函数,先执行 f 再执行 g。这对应“等 f 的结果出来后,把结果交给 g”。
    x↦g(f(x))\displaystyle x \mapsto g(f(x))

语法糖原理

现在你知道了 Promise 链式调用很好用,但写多了还是会觉得有点啰嗦:每个 then 都要包一层回调函数。于是餐厅推出了“VIP 座位”——你直接坐下来等,服务员上菜到你面前。这就是 async/await。

async 关键字声明一个异步函数,这个函数总是返回一个 Promise。await 关键字放在一个 Promise 前面,会“暂停”当前 async 函数的执行,直到这个 Promise settle。暂停期间 JavaScript 主线程并没有闲着,它继续处理其他任务;等 Promise 结果出来后,函数再自动恢复执行。

本质上,await 就是 then 回调的语法糖。const data = await fetch(url) 等价于 fetch(url).then(data => {...}),只是把“回调里的代码”重新组织成了“从上到下的顺序代码”。这让异步代码读起来和同步代码几乎一样,大大降低了心智负担。

坐在餐厅的 VIP 座位上,你不需要举着取号单反复核对,服务员会主动把菜端到你面前。async/await 就是这样一种“不打断思路”的写法,但它背后依然是那张取号单——Promise。

⟨await p; next⟩⇝p.then(next)\displaystyle \langle \text{await}\ p; \ \text{next} \rangle \leadsto p.\text{then}(\text{next})
这个式子表示:先等待 p 再继续执行 next,等价于在 p 的 then 回调里执行 next。await 只是把回调变成了平铺的语句。
  1. 等待指令\langle \text{await}\ p; \ \text{next} \rangle 表示程序执行到 await 时暂停,等待 p 完成后再执行下一步 next。
    ⟨await p; next⟩\displaystyle \langle \text{await}\ p; \ \text{next} \rangle
  2. 等价转换\leadsto 表示“可以等价变形为”,就像把一种写法翻译成另一种写法。
    ⇝\displaystyle \leadsto
  3. 回调写法p.\text{then}(\text{next}) 表示在 p 成功后的回调里执行 next,这是没有 await 时的传统写法。
    p.then(next)\displaystyle p.\text{then}(\text{next})

错误处理

就算取号单再方便,菜也可能做不出来。最怕的是服务员把“做不了”的消息藏在角落,你等了一小时才发现。Promise 的 catch 和 async/await 的 try/catch,就是专门负责把失败原因及时送到你耳边。

在 Promise 链中,任何一个环节抛出的错误,都会被后面的 catch 捕获。这就像传菜过程中,任何一桌出了问题,服务员都会通过对讲机通知领班。在 async/await 中,你只需要把 await 语句包在 try/catch 里,错误就自动变成 catch 的参数。给 throw 一个值,或者 await 一个 rejected 的 Promise,都会触发这个机制。

需要注意的是,如果错误没有 catch 处理,就会变成“未捕获的 Promise 异常”,可能导致程序崩溃或难以排查的现象。这就像服务员喊了三声没人应,最后只能把菜倒掉,问题被无声吞没。finally 则负责“总会执行”的收尾工作,比如关闭加载动画、释放资源。

错误处理就像餐厅的应急预案:菜做砸了,不仅要说“抱歉”,还要告诉你原因、给你一个补偿方案。Promise 的 catch 就是那个主动道歉的服务员,而 try/catch 则让你能安心坐着,把“意外”交给兜底机制。

throw e  ≡  reject(e)\displaystyle \text{throw}\ e \;\equiv\; \text{reject}(e)
在 Promise 语境下,主动抛出一个异常 e,等价于调用 reject(e),错误会沿着链向后传播。
  1. 抛错动作\text{throw}\ e 表示代码主动抛出一个错误 e,比如校验失败时触发。
    throw e\displaystyle \text{throw}\ e
  2. 等价符号\equiv 表示两者在 Promise 错误处理机制中的行为完全一致。
    ≡\displaystyle \equiv
  3. 拒绝调用\text{reject}(e) 表示把 Promise 推向 rejected 状态,并携带错误 e 作为原因。
    reject(e)\displaystyle \text{reject}(e)

并行串行

你请朋友吃饭,一口气点了五道菜。如果让厨房一道一道做,等最后一个菜上桌前面的早就凉了。高效的方式是:五道菜同时下锅,全部上齐再开席。这就是 Promise.all 的并行思维。而有些菜必须按顺序吃,比如先喝汤再吃主菜,这就是串行控制。

Promise.all([p1, p2, p3]) 接收一个 Promise 数组,返回一个新 Promise。只有数组里所有 Promise 都 fulfilled,它才 fulfilled,结果是一个对应顺序的数组;只要有一个 rejected,它就立刻 rejected。这就像你点了五道菜,服务员必须等全部出锅才一起端上来,有一道做砸了,整桌都受影响。

Promise.race([p1, p2, p3]) 则相反,谁先 settle 就用谁的结果。这像你同时叫了三家外卖,谁先送到吃谁的,其余的直接取消。串行控制则需要用 for 循环配合 await 逐条处理,或者用 then 链一层层连接,确保前一个完成后再启动下一个。

把异步任务想象成餐厅的出菜流程:需要同时上的菜就用 Promise.all,需要比速度的就用 Promise.race,必须一碟一碟上的就用串行控制。选对组合方式,才能让整个流程既高效又稳定。

Promise.all([p1,…,pn])=fulfilled([v1,…,vn])若所有 pi 都成功\displaystyle \text{Promise.all}([p_1,\dots,p_n]) = \text{fulfilled}([v_1,\dots,v_n]) \quad \text{若所有 } p_i \text{ 都成功}
这个式子说明 Promise.all 把所有 Promise 的成功值收集成一个数组,并且保持传入顺序不变。
  1. 并行数组[p_1,\dots,p_n] 表示一个包含 n 个 Promise 的数组,相当于 n 张取号单同时发出去。
    [p1,…,pn]\displaystyle [p_1,\dots,p_n]
  2. 整体成功= \text{fulfilled}([v_1,\dots,v_n]) 表示当所有 Promise 都成功时,整体进入 fulfilled,并携带每个结果值组成的数组。
    fulfilled([v1,…,vn])\displaystyle \text{fulfilled}([v_1,\dots,v_n])
  3. 条件判断\quad \text{若所有 } p_i \text{ 都成功} 是数学里的条件说明,强调这是“全员通过”才成立的结果。
    若所有 pi 都成功\displaystyle \text{若所有 } p_i \text{ 都成功}

回调地狱

在 Promise 出现之前,异步代码长什么样?想象你去一家没有取号单的老餐厅,想点三个菜,服务员说:“你先等第一个菜,第一个菜好了,我再帮你去问第二个菜,第二个菜好了,我再帮你问第三个菜。”于是你只能坐在厨房门口,手里攥着三张写着上个服务员电话的纸条,一层套一层,看都看不懂。

这种“回调里套回调”的写法被称为回调地狱。每一层缩进都代表一个异步依赖,错误处理更是噩梦:每个回调都得单独判断有没有出错,稍不留神就给漏了。代码的可维护性极差,改一个逻辑要顺着嵌套往里钻。

Promise 的链式调用把嵌套拍平了。每一层 then 都在同一缩进级别上,错误统一交给 catch,流程一目了然。async/await 则更进一步,用同步语法的外壳包裹异步逻辑,让代码读起来就像菜谱上的步骤清单。从回调地狱到 Promise,再到 async/await,核心思路始终是:把“先后顺序”显式地表达出来,而不是藏在嵌套里。

餐厅升级了取号系统后,你再也不用攥着一把纸条挤在厨房门口。Promise 和 async/await 就是这套现代取号系统,它们让异步代码从“迷宮”变成了“直线”。

---

f(g(h(x)))⏟嵌套回调  ↦  f.then(g).then(h)\displaystyle \underbrace{f(g(h(x)))}_{\text{嵌套回调}} \;\mapsto\; f.\text{then}(g).\text{then}(h)
这个式子表示:原本三层嵌套的函数调用,可以映射成三次 then 的链式调用,从物理上消除缩进。
  1. 嵌套表达式f(g(h(x))) 表示三个异步操作层层包裹,必须从最内层 h 开始执行。
    f(g(h(x)))\displaystyle f(g(h(x)))
  2. 标注\underbrace{\dots}_{\text{嵌套回调}} 用花括号和文字标出这是造成阅读困难的嵌套结构。
    …⏟嵌套回调\displaystyle \underbrace{\dots}_{\text{嵌套回调}}
  3. 映射箭头\mapsto 表示从旧写法转换为新写法。
    ↦\displaystyle \mapsto
  4. 链式表达f.\text{then}(g).\text{then}(h) 表示把三个操作按顺序平铺,每个 then 只依赖前一个结果。
    f.then(g).then(h)\displaystyle f.\text{then}(g).\text{then}(h)

AI 应用场景

前端可视化数据加载

阶段:数据接入与展示

解决的问题:AI 模型推理通常需要几百毫秒甚至几秒,如果同步等待,页面会卡死,用户体验极差。传统回调方案容易把加载逻辑和 UI 更新混在一起,难以维护。

具体案例:一个 AI 图像识别页面,点击“识别”按钮后,前端用 async/await 调用后端推理接口。await 等待期间页面显示加载动画;拿到结果后用 try/catch 捕获接口异常,最后用 finally 关闭加载动画,再更新图表展示识别结果。

并行调用多个模型服务

阶段:模型推理加速

解决的问题:一次智能问答可能需要同时调用语音识别、意图分类、知识检索三个服务。串行调用会累加延迟,用 Promise.all 并行发起请求能把总耗时降到最慢的那个服务耗时。

具体案例:智能客服机器人接收用户语音后,前端并行调用三个 AI 服务:语音转文字、情感分析、商品推荐。Promise.all 等待三个结果都返回后,统一渲染答案卡片。如果某个服务超时,用 Promise.race 设置超时兜底,避免用户无限等待。

数据管道串行处理

阶段:数据预处理

解决的问题:清洗和转换数据时,某些操作必须严格按顺序执行,比如先翻译再摘要,不能并行。用 async/await 配合 for 循环可以逐条处理,同时保持代码清晰。

具体案例:批量翻译场景中,先对每篇文章调用翻译服务,翻译完成后再调用摘要服务,最后写入数据库。用 for...of 循环配合 await 逐篇处理,避免同时发起太多请求导致服务限流。

Python 演示

python77 行
class SimplePromise:    def __init__(self, executor):        self._state = 'pending'        self._value = None        self._handlers = []        try:            executor(self._resolve, self._reject)        except Exception as exc:            self._reject(exc)     def _resolve(self, value):        if self._state != 'pending':            return        self._state = 'fulfilled'        self._value = value        self._flush()     def _reject(self, reason):        if self._state != 'pending':            return        self._state = 'rejected'        self._value = reason        self._flush()     def _flush(self):        while self._handlers:            handler = self._handlers.pop(0)            self._invoke(handler)     def _invoke(self, handler):        on_fulfilled, on_rejected, resolve, reject = handler        try:            if self._state == 'fulfilled':                if on_fulfilled is not None:                    result = on_fulfilled(self._value)                else:                    result = self._value            else:                if on_rejected is not None:                    result = on_rejected(self._value)                else:                    reject(self._value)                    return            resolve(result)        except Exception as exc:            reject(exc)     def then(self, on_fulfilled=None, on_rejected=None):        def executor(resolve, reject):            self._handlers.append((on_fulfilled, on_rejected, resolve, reject))            if self._state != 'pending':                self._flush()        return SimplePromise(executor)     def catch(self, on_rejected):        return self.then(None, on_rejected)     def finally(self, on_done):        def passthrough(value):            on_done()            return value         def rethrow(reason):            on_done()            raise reason         return self.then(passthrough, rethrow)  if __name__ == '__main__':    def async_task(name):        print('开始处理:', name)        return SimplePromise(lambda resolve, reject: resolve(name + '完成'))     p = async_task('取号单A')    p.then(lambda v: print('第一个then:', v)).then(lambda v: print('第二个then:', v))    p.catch(lambda e: print('捕获错误:', e))
目的:用 Python 模拟一个极简的 Promise 类,演示 pending/fulfilled/rejected 三种状态、状态不可逆、then/catch/finally 链式调用和值传递。
  1. self._state = 'pending'初始化 Promise 状态为等待中,相当于取号单刚打印出来。
  2. executor(self._resolve, self._reject)立即执行传入的函数,并把两个内部方法交给它,模拟 JavaScript 的 executor。
  3. if self._state != 'pending': return状态一旦改变,后续 resolve/reject 调用全部忽略,保证不可逆性。
  4. self._handlers.append(...)把 then 注册的回调和新的 resolve/reject 绑定起来,形成链式调用的基础。
  5. return SimplePromise(executor)then 返回一个新的 Promise,让调用可以继续挂下去。
  6. _flush()状态 settle 后,依次执行所有已注册的处理器,相当于服务员开始叫号。
  7. rethrow(reason): on_done(); raise reasonfinally 中如果原来的 Promise 失败,执行完清理后继续抛出错误,不吞掉异常。

常见误区

误区我以为 `resolve()` 之后如果代码又执行了 `reject()`,Promise 的状态会变成 rejected,或者后面的调用会覆盖前面的结果。总之状态是可以改的。

正解Promise 的状态只能从 pending 变为 fulfilled 或 rejected,且只能变一次。一旦状态变为 fulfilled,再调用 reject 会被静默忽略,反之亦然。这就像取号单已经被服务员喊了“32号上菜”,之后就算厨师说鱼卖完了,菜也照样端上桌。

为何常见:因为普通变量可以反复赋值,人们容易把 Promise 也当成一个“可改写的容器”,忽略了它设计上的不可逆性。

误区我以为 `await` 会像 Python 的 `time.sleep()` 一样,把整个主线程卡住,等到异步操作完成才继续执行。

正解await 只暂停当前 async 函数内部后续代码的执行,不会阻塞 JavaScript 主线程。主线程依然可以响应点击、渲染动画。这就像你在座位上等菜,厨房里其他菜照常做,其他客人照常吃。

为何常见:因为代码看起来像同步的,初学者容易从“视觉上的同步”推出“实际上的阻塞”,忽略了事件循环的存在。

误区我以为 `finally` 一定会等到 `catch` 执行完之后才执行,所以如果 `catch` 里又抛了错,`finally` 就不跑了。

正解finally 无论 Promise 成功还是失败都会执行,而且如果 catch 中再次抛错,finally 依然会执行,之后新错误继续向外传播。它就像餐厅的“买单”环节,不管菜好不好吃,有没有投诉,临走前都要买单。

为何常见:很多人按字面理解“catch 处理错误,finally 作收尾”,却忽略了 finally 的“无论怎样都执行”这个更强语义。

误区我以为 `Promise.all` 会等数组里所有 Promise 都结束,哪怕其中一个已经 rejected,也要等全部完成再统一报错。

正解Promise.all 是“快速失败”的:只要有一个 Promise rejected,它立刻返回一个 rejected 的新 Promise,不会等待其余 Promise 结束。这就像一桌菜里有一道做砸了,服务员马上告诉你整桌预订取消,剩余几道菜不用等了。

为何常见:名字里带“all”,人们容易联想到“全部完成才处理”,忽略了它内部实现时的短路机制。

面试题

Q1. 一个 Promise 刚被创建时,它的状态是?

单选题 · easy

  • A.等待中(pending)← 正确答案
  • B.成功(fulfilled)
  • C.失败(rejected)
  • D.不确定状态

答案:A。Promise 创建后立即进入 pending 状态,就像取号单刚打印出来,还没被服务员喊号。只有调用 resolve 或 reject 后才会进入后续状态。

Q2. Promise 状态从 pending 变为 fulfilled 后,再次调用 resolve() 会怎样?

单选题 · easy

  • A.再次触发 then 回调
  • B.被静默忽略← 正确答案
  • C.抛出运行时错误
  • D.状态变成失败

答案:B。Promise 状态一旦 settle 就不可变,后续 resolve 或 reject 调用都会失效。取号单已经被喊过一次号,再喊多少次也没用。

Q3. 执行 promise.then(f).catch(g).finally(h),若 f 抛出异常,哪些回调会执行?

单选题 · mid

  • A.只有 f 会执行
  • B.只有 g 会执行
  • C.g 和 h 都会执行← 正确答案
  • D.只有 h 会执行

答案:C。f 抛错后,链上的 catch 会捕获这个错误,所以 g 执行;finally 无论成功失败都会执行,所以 h 也执行。这像菜做砸了,服务员来道歉(g),然后你还是要买单(h)。

Q4. 一个 async 函数的返回值默认会被包装成什么?

单选题 · mid

  • A.一个 Promise 对象← 正确答案
  • B.普通原始值
  • C.null 值
  • D.undefined 值

答案:A。async 函数总是返回 Promise,即使函数内部只返回一个普通值,也会自动包裹成 fulfilled 的 Promise。这相当于不管你要不要,餐厅都会给你一张取号单作为凭证。

Q5. 在 async 函数中 await 一个已经 fulfilled 的 Promise,会发生什么?

单选题 · hard

  • A.阻塞整个 JS 线程
  • B.暂停当前 async 函数← 正确答案
  • C.立即抛出异常
  • D.跳过 await 继续执行

答案:B。await 会暂停当前 async 函数,把后续代码放到微任务队列里,即使 Promise 已成功也一样要等下一个事件循环周期。JS 主线程不会阻塞,其他任务照常运行。

Q6. 关于 Promise.all 的特点,下列哪些说法正确?

多选题 · mid

  • A.全部成功则整体成功← 正确答案
  • B.任一失败则快速失败← 正确答案
  • C.同时等待所有 Promise← 正确答案
  • D.由最先完成者决定结果

答案:ABC。Promise.all 会等待所有 Promise 完成,全部成功时返回结果数组;任何一个失败会立刻短路失败。D 描述的是 Promise.race 的行为,不是 all。

Q7. 关于 finally,下列说法正确的是?

多选题 · hard

  • A.无论成功失败都会执行← 正确答案
  • B.能够覆盖链式返回的值
  • C.原状态和值会被保留← 正确答案
  • D.会自动吞掉错误

答案:AC。finally 的回调不接收结果,返回值也会被忽略,所以不能覆盖值和状态;错误会继续向后传播,不会被吞掉。它像“买单”动作,不影响菜的评价。

Q8. catch(onRejected) 等价于 then 的哪个用法?

单选题 · easy

  • A.第一个参数传 onRejected
  • B.第二个参数传 onRejected← 正确答案
  • C.第三个参数传 onRejected
  • D.与 then 完全无关系

答案:B。then 接受两个参数,第二个参数就是 rejected 时的回调。catch 是 then(undefined, onRejected) 的简写,就像把失败处理单独抽出来。

Q9. 串行控制多个异步任务时,哪些做法是正确的?

多选题 · mid

  • A.在 then 中返回新 Promise← 正确答案
  • B.用 for 循环配合 await← 正确答案
  • C.用 Promise.all 全部并行
  • D.在回调中继续嵌套下一层

答案:AB。A 和 B 都能保证前一个任务完成后才启动下一个,是串行控制。C 是并行,不满足串行;D 会形成回调地狱,虽然能跑但极难维护。

Q10. 一个 async 函数内部主动 throw 一个错误,调用该函数会返回什么?

单选题 · extreme

  • A.一个 rejected 的 Promise← 正确答案
  • B.一个 fulfilled 的 Promise
  • C.直接抛出同步错误
  • D.返回 undefined 值

答案:A。async 函数中的 throw 会被引擎捕获,并自动转换成 rejected 的 Promise,而不是同步抛错。这相当于厨房把“做不了”划在取号单上,而不是直接把单子扔到你脸上。