从零开始理解 JavaScript Promise:彻底搞懂异步编程

🎯 从零开始理解 JavaScript Promise:彻底搞懂异步编程

🔗 原文链接:Promises From The Ground Up

👨‍💻 原作者:Josh W. Comeau

📅 发布时间:2024年6月3日

🕐 最后更新:2025年3月18日
⚠️ 关于本译文

本文基于 Josh W. Comeau 的原文进行忠实翻译,力求准确传达原作者的技术观点和逻辑结构。

🎨 特色亮点:

  • 保持原文的完整性和技术准确性
  • 采用自然流畅的中文表达,避免翻译腔
  • 添加画外音板块,提供译者的补充解读和实践心得
  • 使用生动比喻帮助理解复杂概念

💡 画外音说明: 文中标注为画外音的部分是译者基于实际开发经验添加的拓展解释,旨在帮助读者更好地理解和应用这些概念,不代表原作者观点。


在学习 JavaScript 的路上,有很多坎儿要过。其中最大、最让人头疼的,就是 Promise(承诺)

要想真正理解 Promise,咱们得对 JavaScript 的工作原理和它的局限性有相当深入的了解。没有这些背景知识,Promise 就像天书一样难懂。

这事儿确实挺让人抓狂的,因为 Promise API 在现代 JavaScript 开发中实在太重要了。它已经成为处理异步代码的标准方式。现代的 Web API 都是建立在 Promise 之上的。没办法绕过去:如果想用 JavaScript 高效工作,真的很有必要搞懂 Promise。

所以在这篇教程里,咱们要学习 Promise,但会从最基础的地方开始。我会分享那些花了我好几年才搞明白的关键知识点。希望读到最后,你能对 Promise 是什么、怎么有效使用它们有更深的理解。✨

💡 画外音 :我刚开始学 Promise 的时候,总是记不住 .then().catch() 到底该怎么用,感觉像是硬记 API。后来才明白,一旦理解了 JavaScript 单线程的本质和异步编程的必要性,这些 API 设计就显得非常自然了。所以这篇文章真的是从根源讲起,强烈推荐耐心读完。
适合谁看

这篇文章适合初级到中级的 JavaScript 开发者。你需要懂一些基本的 JavaScript 语法。


🤔 为啥要这么设计?

假设咱们想做一个"新年倒计时",像这样的效果:

如果 JavaScript 和大多数其他编程语言一样,咱们可以这样解决问题:

javascript 复制代码
function newYearsCountdown() {
  print("3");
  sleep(1000);

  print("2");
  sleep(1000);

  print("1");
  sleep(1000);

  print("Happy New Year! 🎉");
}

在这段假想的代码里,程序会在遇到 sleep() 调用时暂停,等指定的时间过去后再继续执行。

可惜的是,JavaScript 里没有 sleep 函数,因为它是一门单线程语言。*

💡 画外音:这里的"线程"(thread)指的是执行代码的长时间运行进程。JavaScript 只有一个线程,所以它一次只能做一件事,不能同时处理多个任务。这是个问题,因为如果咱们唯一的 JavaScript 线程忙着管理倒计时,它就干不了别的事儿了。

*技术上讲,现代 JavaScript 可以通过 Web Workers 访问多线程,但这些额外的线程无法访问 DOM,所以在大多数场景下用不上。

当我刚学这些东西的时候,不太明白为什么这是个问题。如果倒计时是现在唯一发生的事情,那 JS 线程在这段时间被完全占用不是挺正常的吗?

嗯,虽然 JavaScript 没有 sleep 函数,但它确实有一些其他函数会长时间占用主线程。咱们可以用这些方法来体验一下,如果 JavaScript 真有 sleep 函数会是什么样子。

比如说,window.prompt()。这个函数用来从用户那里收集信息,它会暂停代码执行,就像咱们假想的 sleep() 函数一样。

点击下面这个示例中的按钮,然后在提示框打开时试着和页面交互

💡 提示 :这里只放了截图。如果想亲自体验这个效果(强烈推荐!),可以去原文页面试试,点击按钮后你会发现整个页面真的卡住了,完全动不了。

注意到了吗?当提示框打开的时候,整个页面完全没反应!你没法滚动、点击任何链接,也没法选择任何文本!JavaScript 线程正忙着等咱们输入值,好让它能继续运行代码。在等待的过程中,它干不了别的任何事,所以浏览器就把整个 UI 都锁住了。

其他语言有多个线程,所以其中一个被占用一会儿也没啥大不了的。但在 JavaScript 里,咱们就这一个线程,而且它要用来干所有事情:处理事件、管理网络请求、更新 UI 等等。

如果想做一个倒计时,咱们得找个不阻塞线程的方法。

💡 画外音:这就是为什么你有时会看到有人说"不要在主线程做耗时操作"。比如复杂的计算、大数据处理,如果放在主线程,用户就会感觉页面卡死了。这也是为什么后来出现了 Web Workers,专门用来处理这类重活儿。
为什么整个 UI 都冻结了?

在上面 window.prompt() 的例子中,浏览器等待咱们输入值的时候,整个 UI 都变得没反应了。

这有点奇怪......浏览器滚动页面或选择文本又不依赖 JavaScript。那为什么这些操作也做不了呢?

我觉得浏览器这么做是为了防止 bug。比如滚动页面会触发 "scroll" 事件,这些事件可以被 JavaScript 捕获和处理。如果 JS 线程忙着的时候滚动事件发生了,那段代码就永远不会运行,如果开发者假设滚动事件总是会被处理,就可能导致 bug。

这也可能是出于用户体验的考虑;也许浏览器禁用 UI 是为了让用户不能忽略提示框。不管怎样,我估计原生的 sleep 函数也得这么工作才能防止 bug。


📞 回调函数(Callbacks)

咱们工具箱里解决这类问题的主要工具是 setTimeoutsetTimeout 是一个接受两个参数的函数:

  1. 未来某个时刻要做的一块工作
  2. 要等待的时间

来看个例子:

javascript 复制代码
console.log('Start');

setTimeout(
  () => {
    console.log('After one second');
  },
  1000
);

这块工作通过一个函数传进去。这种模式叫做回调(callback)

前面假想的 sleep() 函数就像给公司打电话,然后一直等着接通下一个客服。而 setTimeout() 就像按 1 让他们在客服有空的时候给你回电。你可以挂掉电话,该干嘛干嘛。

setTimeout() 被称为异步 函数。这意味着它不会阻塞线程。相比之下,window.prompt()同步的,因为 JavaScript 线程在等待的时候干不了别的。

异步代码的一个大坑是,它意味着咱们的代码不会总是按线性顺序运行。看看下面这个例子:

javascript 复制代码
console.log('1. Before setTimeout');

setTimeout(() => {
  console.log('2. Inside setTimeout');
}, 500);

console.log('3. After setTimeout');

你可能期望这些日志按从上到下的顺序触发:1 > 2 > 3但记住,回调的核心思想就是"留个号,一会儿回你。 JavaScript 线程不会干坐着等,它会继续运行。

想象一下,如果咱们给 JavaScript 线程一本日记,让它记录运行这段代码时做的所有事情。运行完之后,日记会是这样:

  • 00:000:打印 "1. Before setTimeout"
  • 00:001:注册一个定时器
  • 00:002:打印 "3. After setTimeout"
  • 00:501:打印 "2. Inside setTimeout"

setTimeout() 注册了回调,就像在日历上安排一个会议。注册回调只需要极短的时间,一旦完成,它就继续往下走,执行程序的其余部分。

💡 画外音 :这个"日记"的比喻特别好,帮我彻底理解了事件循环。很多新手(包括当年的我)觉得 setTimeout(fn, 0) 很神奇------明明延迟是 0,为什么还是异步的?就是因为它会被"注册"到日历上,即使时间到了,也得等当前同步代码都跑完才轮到它。

回调在 JavaScript 里到处都是,不只是用于定时器。比如,咱们这样监听指针事件(pointer events):

💡 画外音:"pointer"(指针)是个统称,涵盖了所有涉及"指向"的 UI 输入方式,包括鼠标、手指在触摸屏上的点击、触控笔等。所以 pointer events 比 mouse events 的概念更广。

window.addEventListener() 注册了一个回调,每当检测到特定事件时就会被调用。在这个例子中,咱们监听鼠标移动。每当用户移动鼠标或在触摸屏上拖动手指,咱们就会运行一块代码作为响应。

就像 setTimeout 一样,JavaScript 线程不会专注于监视和等待这些事件。它告诉浏览器"嘿,用户移动指针的时候告诉我一声"。当事件触发时,JS 线程会回过头来运行咱们的回调。

好吧,咱们已经跑得有点远了。回到最初的问题:如果想做一个 3 秒倒计时,该怎么做?

在过去,最常见的解决方案是设置嵌套的回调,像这样:

javascript 复制代码
console.log("3...");

setTimeout(() => {
  console.log("2...");

  setTimeout(() => {
    console.log("1...");

    setTimeout(() => {
      console.log("Happy New Year!!");
    }, 1000);
  }, 1000);
}, 1000);

这太疯狂了,对吧?咱们的 setTimeout 回调里又创建了新的 setTimeout 回调!

当我在 2000 年代早期开始折腾 JavaScript 的时候,这种模式挺常见的,虽然大家都觉得不太理想。咱们把这种模式叫做回调地狱(Callback Hell)

Promise 就是为了解决回调地狱的一些问题而开发的。

💡 画外音:回调地狱不仅仅是代码难看的问题。真正的痛点是:错误处理变得超级复杂,每层嵌套都要处理错误;代码的可读性和维护性极差,嵌套超过 3 层基本就看不懂了。我曾经维护过一个 7 层嵌套的回调,那酸爽,现在想起来还头疼。
等等,定时器怎么知道什么时候触发?

setTimeout API 接收一个回调函数和一个持续时间。过了指定时间后,回调函数就会被调用。

但怎么做到的?如果 JavaScript 线程没有看着定时器,像老鹰盯小鸡一样盯着它,它怎么知道该调用回调了?

这超出了本教程的范围,但 JavaScript 有个东西叫做事件循环(event loop) 。当咱们调用 setTimeout 时,一条小消息会被添加到队列里。每当 JS 线程不在执行代码时,它就在监视事件循环,检查消息。

定时器到期时,事件循环里就会亮起一个提示灯,就像有新留言的答录机。如果 JS 线程当时没在忙,它会立刻跳过去执行传给 setTimeout() 的回调。

这确实意味着定时器不是 100% 精确的。JavaScript 只有一个线程,它可能正忙着干别的事儿,比如处理滚动事件或等待 window.prompt()。如果咱们指定了 1000ms 的定时器,可以确信至少过了 1000 毫秒,但可能会稍微长一点。

你可以在 MDN 上了解更多关于事件循环的内容。


🎁 Promise 登场

前面说过,咱们不能让 JavaScript 傻等着再执行下一行代码,因为那会把线程堵死。得想办法把工作拆成一块块异步执行。

不过嵌套太难看了,能不能换个思路?要是能把这些操作像串珠子一样连起来就好了------先做这个,做完了做那个,再做下一个。

就当好玩儿,咱们假设有根魔法棒,可以随意改变 setTimeout 函数的工作方式。如果咱们这样做会怎样:

javascript 复制代码
console.log('3');

setTimeout(1000)
  .then(() => {
    console.log('2');

    return setTimeout(1000);
  })
  .then(() => {
    console.log('1');

    return setTimeout(1000);
  })
  .then(() => {
    console.log('Happy New Year!!');
  });

不直接把回调传给 setTimeout(那会导致嵌套和回调地狱),而是用一个特殊的 .then() 方法把它们串起来,是不是好多了?

这就是 Promise 的核心思想。Promise 是 JavaScript 在 2015 年一次大更新中加入的特殊结构。

可惜 setTimeout 还是老样子,用的是回调风格。因为 setTimeout 在 Promise 出现之前就已经存在很久了,要是改了它的工作方式,会导致很多老网站挂掉。向后兼容是好事,但也意味着有些东西没法那么优雅。

不过现代的 Web API 都是基于 Promise 构建的。咱们来看个例子。


🔧 使用 Promise

fetch() 函数允许咱们发起网络请求,通常是从服务器获取一些数据。

看看这段代码:

javascript 复制代码
const fetchValue = fetch('/api/get-data');

console.log(fetchValue);
// -> Promise {<pending>}

当咱们调用 fetch() 时,它启动网络请求。这是一个异步操作,所以 JavaScript 线程不会停下来等待。代码继续运行。

fetch() 函数到底返回了啥?肯定不是服务器返回的真实数据,因为咱们才刚发起请求,数据还在路上呢。它返回的其实是一张"欠条"(IOU),就像浏览器给你打的一张白条,上面写着:"嘿,数据我还没拿到,但我保证马上就给你!"

💡 画外音:IOU 是 "I Owe You"(我欠你)的缩写,读音就像说"I Owe You"。它是一种表示欠债的凭据。用这个比喻特别贴切------Promise 就像浏览器给你打的一张欠条:"数据我现在还没拿到,但我欠你的,到时候一定给你"。

具体来说,Promise 就是个 JavaScript 对象。它内部永远只会处于三种状态之一:

  • pending(待定) --- 工作正在进行中,还没完成
  • fulfilled(已完成) --- 工作已成功完成
  • rejected(已拒绝) --- 出了点问题,Promise 无法完成

只要 Promise 还在 pending 状态,就说它是未解决的(unresolved)。一旦工作完成了,它就变成已解决(resolved)。这里要注意:不管最后是成功(fulfilled)还是失败(rejected),都算是"解决了"。

💡 画外音:Promise 的这三种状态一开始可能有点绕。我喜欢这样理解:pending 就像快递在路上,fulfilled 就像快递送到了,rejected 就像快递丢了或地址错了。一旦快递状态确定(送到或丢失),就不会再变了。

一般来说,咱们会希望在 Promise 完成后做点什么。这时候就用 .then() 方法:

javascript 复制代码
fetch('/api/get-data')
  .then((response) => {
    console.log(response);
    // Response { type: 'basic', status: 200, ...}
  });

fetch() 返回一个 Promise,咱们用 .then() 挂上一个回调函数。等浏览器收到响应了,这个回调就会被执行,响应对象也会作为参数传进来。

等待 JSON?

如果你用过 Fetch API,可能注意到需要第二步才能真正拿到咱们需要的 JSON 数据:

javascript 复制代码
fetch('/api/get-data')
  .then((response) => {
    return response.json();
})
 .then((json) => {
   console.log(json);
   // { data: { ... } }
 });

response.json() 会返回一个全新的 Promise,等响应数据完全转成 JSON 格式后,这个 Promise 才算完成。

但等等,为啥 response.json() 还是异步的?咱们不是已经拿到响应了吗,数据不应该早就是 JSON 了吗?

还真不一定。Web 的一个核心特性是,服务器可以流式传输数据,一点点分批发送。这在传视频(比如 YouTube)的时候很常见,对于大一点的 JSON 数据也可以这么干。

fetch() 返回的 Promise,在浏览器收到第一个字节数据时就算完成了。而 response.json() 的 Promise,要等到收到最后一个字节才算完成。

实际上,JSON 数据很少分批发送,所以这两个 Promise 大多数时候会同时完成。但 Fetch API 在设计时就考虑到了流式响应的场景,所以才需要这么绕一下。
💡 画外音 :新手常犯的一个错误是:拿到 response 后直接用,忘了调用 .json()。记住,fetch() 返回的第一个 Promise 只是给你一个"响应对象",里面的数据还是原始格式,需要再调用 .json() 才能解析成 JavaScript 对象。这也是为什么你经常看到两个 .then() 的原因。


🛠️ 创建自己的 Promise

用 Fetch API 的时候,Promise 是 fetch() 函数在背后帮咱们创建的。但要是咱们用的 API 不支持 Promise 呢?

比如 setTimeout,它是在 Promise 出现之前就有了。要想用定时器又不掉进回调地狱,就得自己动手包装一个 Promise。

语法是这样的:

javascript 复制代码
const demoPromise = new Promise((resolve) => {
  // 做一些异步工作,然后
  // 调用 `resolve()` 来完成 Promise
});

demoPromise.then(() => {
  // 当 Promise 完成时,
  // 这个回调会被调用!
})

Promise 其实是个通用容器,它本身不干活儿。当咱们用 new Promise() 创建 Promise 时,得同时告诉它"你要干啥活儿"------通过传入一个函数来指定具体的异步任务。这个任务可以是任何东西:发网络请求、等个定时器、读个文件,啥都行。

等这个活儿干完了,咱们就调用 resolve(),告诉 Promise:"搞定了,一切顺利!"这样 Promise 就变成已解决状态了。

回到咱们一开始的问题------做个倒计时。在这个场景里,异步任务就是"等 setTimeout 跑完"。

那咱们可以自己动手,写一个基于 Promise 的小工具函数,把 setTimeout 包装一下:

javascript 复制代码
function wait(duration) {
  return new Promise((resolve) => {
    setTimeout(resolve, duration);
  });
}

const timeoutPromise = wait(1000);

timeoutPromise.then(() => {
  console.log('1 second later!')
});

这段代码看起来超级吓人。咱们试着分解一下:

  • 咱们写了个新的工具函数 wait,它接收一个参数 duration(持续时间)。目标是把它当成 sleep 函数用,但是异步的、不阻塞线程的那种。
  • wait 函数里创建并返回了一个新的 Promise。Promise 自己啥也不干,得靠咱们在异步工作完成时调用 resolve
  • Promise 内部,咱们用 setTimeout 启动了一个定时器。把 Promise 给的 resolve 函数和用户传进来的 duration 都给它。
  • 定时器时间到了,就会执行回调。这就形成了连锁反应:setTimeout 执行了 resolveresolve 告诉 Promise "搞定了",然后 .then() 里的回调也跟着被触发。

这段代码要是还让你头疼,别担心😅。这里确实揉了好多高级概念在一起!能理解大概思路就行,细节慢慢消化。

有个点可能会帮你理清楚:上面代码里,咱们把 resolve 函数直接扔给了 setTimeout。其实也可以这样写,创建一个箭头函数来调用 resolve

javascript 复制代码
function wait(duration) {
  return new Promise((resolve) => {
    setTimeout(
      () => resolve(),
      duration
    );
  });
}

JavaScript 里函数是"一等公民",意思是函数可以像字符串、数字那样随便传来传去。这特性挺厉害,但新手可能需要点时间才能习惯。上面这种写法不那么直接,但效果完全一样,哪种看着舒服就用哪种!

💡 画外音 :这个 wait 函数是我在实际项目中常用的一个工具。很多人会把它加到工具函数库里。甚至有些库(比如 p-timeout)专门提供这类 Promise 工具。学会包装旧的回调式 API 成 Promise,这个技能超级有用,因为还有很多老代码和库用的是回调。


⛓️ 链式调用 Promise

关于 Promise,有一点很重要要理解:它们只能被解决一次。一旦 Promise 被完成或拒绝,它就永远保持那个状态了。

这意味着 Promise 并不真正适合某些场景。比如事件监听器:

javascript 复制代码
window.addEventListener('mousemove', (event) => {
  console.log(event.clientX);
})

这个回调会在用户每次移动鼠标时触发,可能成百上千次。Promise 干不了这活儿。

那咱们的倒计时怎么办?虽然不能重复用同一个 wait Promise,但可以把多个 Promise 串成一条链:

javascript 复制代码
wait(1000)
  .then(() => {
    console.log('2');
    return wait(1000);
  })
  .then(() => {
    console.log('1');
    return wait(1000);
  })
  .then(() => {
    console.log('Happy New Year!!');
  });

第一个 Promise 完成了,.then() 回调就被执行。这个回调又创建并返回一个新的 Promise,就这样一个接一个地串下去。

💡 画外音 :Promise 链是个很强大的模式。关键点在于每个 .then() 都会返回一个新的 Promise,这样就能一直链下去。不过要注意:如果忘记 return,链就断了,后面的 .then() 不会等前面的异步操作完成。这是新手常犯的错误,我也踩过好几次坑。


📦 传递数据

前面的例子里,咱们调用 resolve 时都没传参数,只是用它来标记"活儿干完了"。但有时候,咱们还得把结果数据传出来!

来看个例子,假设有个用回调的数据库库:

javascript 复制代码
function getUser(userId) {
  return new Promise((resolve) => {
    // 在这个例子中,异步工作是
    // 根据 ID 查找用户
    db.get({ id: userId }, (user) => {
      // 现在咱们有了完整的 user 对象,
      // 可以在这里传进去...
      resolve(user);
    });
  });
}

getUser('abc123').then((user) => {
  // ...然后在这里取出来!
  console.log(user);
  // { name: 'Josh', ... }
})

传给 resolve 的参数,会原封不动地传到 .then() 的回调函数里。这样就能把异步操作的结果一路传出去了。


❌ 被拒绝的 Promise

可惜,JavaScript 的世界里,Promise 不是总能兑现。有时候也会黄了。

比如用 Fetch API 发网络请求,不一定能成功啊!可能网络不稳定,也可能服务器挂了。这些情况下,Promise 就会被拒绝(rejected),而不是正常完成。

咱们可以用 .catch() 方法来处理:

javascript 复制代码
fetch('/api/get-data')
  .then((response) => {
    // ...
  })
  .catch((error) => {
    console.error(error);
  });

Promise 成功了,就走 .then() 这条路。失败了,就走 .catch()。可以理解为两条岔路,看 Promise 最后是啥状态。

💡 画外音 :错误处理是 Promise 相比回调的一大优势。在回调地狱里,每层嵌套都要单独处理错误。但用 Promise,你可以在链的末尾加一个 .catch(),它能捕获整个链中任何地方的错误。这大大简化了错误处理逻辑。
Fetch 的坑

假设服务器返回了个错误,比如 404 Not Found 或者 500 Internal Server Error。这应该会触发 Promise 被拒绝,对不对?

意外的是,并不会!这种情况下,Promise 还是会正常完成,只不过 Response 对象里会带着错误信息:

javascript 复制代码
Response {
  ok: false,
  status: 404,
  statusText: 'Not Found',
}

这看着有点奇怪,但仔细想想也说得通:咱们的 Promise 确实完成了,也从服务器拿到响应了!虽然不是咱们想要的那种响应,但确实有响应。

至少按"许三个愿望的精灵"的逻辑,这没毛病。

自己写 Promise 的时候,可以用第二个参数 reject 来标记拒绝:

javascript 复制代码
new Promise((resolve, reject) => {
  someAsynchronousWork((result, error) => {
    if (error) {
      reject(error);
      return;
    }

    resolve(result);
  });
});

Promise 里面要是出了问题,就调用 reject() 来标记失败。传给 reject() 的参数(通常是个错误对象)会被传到 .catch() 回调里。

令人困惑的名字

前面说过,Promise 有三种状态:pending(进行中)、fulfilled(成功)和 rejected(失败)。那为啥参数不叫 "fulfill" 和 "reject",而是叫 "resolve" 和 "reject" 呢?

原因是这样的:resolve() 大多数情况下确实会让 Promise 变成 fulfilled 状态。但有个特殊情况------如果你在 resolve() 里传入的不是普通值,而是另一个 Promise,事情就不一样了。

举个例子:

javascript 复制代码
const promise1 = new Promise((resolve) => {
  const promise2 = fetch('/api/data');
  resolve(promise2); // 传入了另一个 Promise!
});

这时候,promise1 会"挂靠"到 promise2 上,等 promise2 的结果。虽然 promise1 技术上还在 pending 状态,但它已经算是 "resolved"(已交接)了------因为它已经把自己的命运交给 promise2 了,JavaScript 线程也已经去忙 promise2 的事儿了。

所以 "resolved" 不等于 "fulfilled",它更像是"已经有着落了"(不管最后成功还是失败)。

这个细节我也是发完博文后读者告诉我才知道的(感谢大家!)。老实说,99% 的开发者都不会碰到这种情况,不用纠结。如果你真的想深入研究,可以看这个文档:States and Fates
💡 画外音 :说实话,这个"resolved vs fulfilled"的区别在日常开发中真的不太需要纠结,记住 resolve() 表示成功、reject() 表示失败就够了。不过如果你在面试或者读规范文档的时候碰到,至少知道是咋回事。


🎭 Async / Await

现代 JavaScript 最牛的一点就是 async / await 语法。用了这个语法,咱们终于能写出接近理想状态的倒计时代码了:

javascript 复制代码
async function countdown() {
  console.log("5...");
  await wait(1000);

  console.log("4...");
  await wait(1000);

  console.log("3...");
  await wait(1000);

  console.log("2...");
  await wait(1000);

  console.log("1...");
  await wait(1000);

  console.log("Happy New Year!");
}

等等,这不是不可能吗! 函数执行到一半不能暂停啊,那会把线程堵死的!

其实这个新语法底层还是 Promise。咱们来扒开看看它是怎么运作的:

javascript 复制代码
async function addNums(a, b) {
  return a + b;
}

const result = addNums(1, 1);

console.log(result);
// -> Promise {<fulfilled>: 2}

本以为返回值应该是数字 2,结果却是个 Promise,里面包着数字 2 。只要给函数加上 async 关键字,它就一定会返回 Promise,哪怕函数里压根没干异步的活儿。

上面的代码其实是这样的语法糖:

javascript 复制代码
function addNums(a, b) {
  return new Promise((resolve) => {
    resolve(a + b);
  });
}

同样的,await 关键字也是 .then() 回调的语法糖:

javascript 复制代码
// 这段代码...
async function pingEndpoint(endpoint) {
  const response = await fetch(endpoint);
  return response.status;
}

// ...等价于这个:
function pingEndpoint(endpoint) {
  return fetch(endpoint)
    .then((response) => {
      return response.status;
    });
}

Promise 给 JavaScript 打好了底层基础,让咱们能写出看着像同步、实际是异步的代码。

这设计,真的绝了。

💡 画外音async/await 是我最喜欢的 JavaScript 特性之一。它让异步代码读起来就像同步代码一样自然。不过有个常见误区:很多人以为 async/await 是一种新的异步机制,其实它只是 Promise 的语法糖。理解这一点很重要,因为有时候你还是需要直接用 Promise(比如 Promise.all() 并发请求)。另外,别忘了用 try/catch 包裹 await,不然错误可能会悄悄溜走!


🚀 更多内容即将推出!

过去几年,我全职都在做教育内容,制作和分享像这篇博文这样的资源。我已经做了 CSS 课程和 React 课程。

学生们问得最多的就是:"能不能做个原生 JavaScript 的课程?"这事儿我一直在想。接下来几个月,应该会发更多关于原生 JavaScript 的文章。

想在我发布新内容时第一时间知道的话,最好是订阅我的邮件列表。有新博文或者课程更新,我都会发邮件通知你。❤️


📝 译者总结

💡 核心要点回顾

概念 关键理解
单线程本质 JavaScript 只有一个线程,不能像其他语言那样"停下来等"
回调地狱 嵌套回调难以维护,错误处理复杂,这是 Promise 要解决的核心问题
Promise 状态 pending(进行中)→ fulfilled(成功)或 rejected(失败)
链式调用 .then() 返回新 Promise,可以一直链下去,避免嵌套
async/await Promise 的语法糖,让异步代码看起来像同步,但本质还是 Promise

🎯 实用建议

  1. 包装旧 API :很多老 API 还在用回调,学会用 Promise 包装它们(像文中的 wait 函数)
  2. 错误处理 :养成在 Promise 链末尾加 .catch() 的习惯,或者用 try/catch 包裹 await
  3. 别忘了 return :Promise 链中如果需要传递数据或继续链式调用,一定要 return
  4. 并发请求 :需要同时发起多个请求时,用 Promise.all() 而不是多个 await
  5. Fetch 陷阱 :记住 HTTP 错误状态码(404、500等)不会触发 .catch(),要检查 response.ok
相关推荐
崔庆才丨静觅9 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606110 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了10 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅10 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅10 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅11 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment11 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅11 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊11 小时前
jwt介绍
前端
爱敲代码的小鱼11 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax