news 2026/10/24 4:38:34

zh.javascript.info 教程:深入理解 JavaScript Promise 基础——从构造器、executor 到 then/catch/finally

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
zh.javascript.info 教程:深入理解 JavaScript Promise 基础——从构造器、executor 到 then/catch/finally
  • 文档
  • 教程
  • 前端

【免费下载链接】zh.javascript.info

现代 JavaScript 教程(The Modern JavaScript Tutorial),以最新的 ECMAScript 规范为基准,通过简单但足够详细的内容,为你讲解从基础到高阶的 JavaScript 相关知识。

项目地址:https://gitcode.com/gh_mirrors/zh/zh.javascript.info
点击查看免费下载

本篇文章基于开源仓库 zh.javascript.info 中「异步编程」章节的 Promise 基础篇 展开,系统地讲解 Promise 的诞生动机、构造器与 executor 的运行机制、state/result内部状态流转,以及.then/.catch/.finally三大消费方法的完整语义。全文结合仓库内 练习任务 与 示例源码,帮助你彻底理解「生产者代码」如何通过 Promise 把结果交给「消费者代码」,并掌握用 Promise 重构回调风格异步代码(如动态加载脚本)的实战能力。

Promise 解决的问题:生产者与消费者的解耦

在真实项目中,我们经常面对这样一个场景:一段代码(生产者代码)需要花费时间完成工作(例如通过网络加载数据),而另一些代码(消费者代码)希望在结果就绪的第一时间拿到它,并且可能有多个函数同时需要这个结果。

原文档用一个生动的类比解释了这一模型:一位顶尖歌手承诺在单曲发布的第一时间通知所有粉丝,粉丝们只需在「订阅列表」上留下邮箱即可。即使录音室失火导致新歌无法发布,订阅者也会及时收到通知——没有人被催促,也没有人错过消息。对应到编程中:

  1. 生产者代码(producing code)执行耗时任务,如同「歌手」;
  2. 消费者代码(consuming code)希望在结果完成时立刻获得它,如同「粉丝」;
  3. Promise是将两者连接起来的特殊 JavaScript 对象,如同「订阅列表」——生产者在任意时长后产出所承诺的结果,Promise 再把结果开放给所有已订阅的代码。

需要说明的是,这个类比并不完全精确:JavaScript 的 Promise 比简单的订阅列表功能更丰富,也有限制,但作为入门理解已经足够。

Promise 构造器与 executor

Promise 对象的构造器语法如下:

let promise = new Promise(function(resolve, reject) { // executor(生产者代码,"歌手") });

传递给new Promise的函数被称为executor。它有两个关键行为:

  • 自动执行:当new Promise被创建时,executor 会立即自动运行;
  • 只写逻辑:executor 内部只包含最终应产出结果的生产者代码,而resolve与reject这两个参数是由 JavaScript 引擎预先定义好的回调,我们无需(也不能)自行创建,只需要在合适的时机调用其中之一。

当 executor 获得结果——无论早还是晚——它应当调用以下回调之一:

  • resolve(value)—— 任务成功完成,携带结果value;
  • reject(error)—— 出现错误,error通常是一个 error 对象。

state 与 result:Promise 的内部状态

由new Promise构造器返回的 promise 对象拥有两个内部属性:

属性初始值调用resolve(value)后调用reject(error)后
state"pending"(待定)"fulfilled"(已成功)"rejected"(已失败)
resultundefinedvalueerror

executor 最终会把 promise 迁移到"fulfilled"或"rejected"两者之一:

与初始的"pending"相对,无论 promise 最终是 resolved 还是 rejected,都统称为settled(已敲定)。

一个完整的成功示例

下面这段代码中,executor 包含带setTimeout的「生产者代码」:

let promise = new Promise(function(resolve, reject) { // 当 promise 被构造完成时,自动执行此函数 // 1 秒后发出工作已经完成的信号,并带有结果 "done" setTimeout(() => resolve("done"), 1000); });

运行这段代码可以观察到两件事:

  1. executor 被自动且立即调用(通过new Promise);
  2. executor 接受的resolve和reject由 JavaScript 引擎预先定义,我们只需要在准备好的时候调用其中之一。

经过 1 秒的「处理」后,executor 调用resolve("done")产生结果,promise 对象状态随之改变:

一个失败示例

下面则是 executor 以 error 拒绝 promise 的示例:

let promise = new Promise(function(resolve, reject) { // 1 秒后发出工作已经完成的信号,并带有 error setTimeout(() => reject(new Error("Whoops!")), 1000); });

对reject(...)的调用将 promise 的状态迁移至"rejected":

必须牢记的四条关键规则

规则一:只有一个结果或一个 error

executor 只能调用一次resolve或一次reject,任何状态的更改都是最终的。所有再对resolve和reject的调用都会被忽略:

let promise = new Promise(function(resolve, reject) { resolve("done"); reject(new Error("…")); // 被忽略 setTimeout(() => resolve("…")); // 被忽略 });

这一点在仓库练习 01-re-resolve 中被直接验证:下面的代码输出1而非2,因为第二个resolve调用会被忽略,只有第一次对reject/resolve的调用才会被处理:

let promise = new Promise(function(resolve, reject) { resolve(1); setTimeout(() => resolve(2), 1000); }); promise.then(alert);

另外,resolve/reject只需要一个参数(或不包含任何参数),额外的参数会被忽略。

规则二:建议以 Error 对象 reject

如果出现问题,executor 应当调用reject。虽然可以像resolve一样传入任何类型的参数,但建议使用Error对象(或继承自Error的对象)——原因在后续学习错误处理链(.catch与throw的配合)时会很快变得显而易见。

规则三:resolve/reject 可以立即进行

executor 通常是异步执行某些操作、一段时间后再调用resolve/reject,但这并非必须:

let promise = new Promise(function(resolve, reject) { // 不花时间去做这项工作 resolve(123); // 立即给出结果:123 });

例如,当我们开始一个任务,随后发现一切都已经完成并已被缓存时,就可能出现这种情况。此时我们立即就拥有了一个 resolved 的 promise。

规则四:state 和 result 都是内部的

Promise 对象的state和result属性都是内部的,我们无法直接访问它们,只能通过.then/.catch/.finally方法间接消费。下面就来介绍这些方法。

消费者:then 与 catch

Promise 对象充当 executor(生产者)和消费函数(粉丝)之间的连接。消费函数通过.then和.catch方法注册。

then:同时注册成功与失败处理

.then是最重要、最基础的方法,语法如下:

promise.then( function(result) { /* 处理成功结果 */ }, function(error) { /* 处理 error */ } );
  • 第一个参数:promise resolved 并接收到结果后执行;
  • 第二个参数:promise rejected 并接收到 error 后执行。

对成功 resolved 的 promise 做出反应:

let promise = new Promise(function(resolve, reject) { setTimeout(() => resolve("done!"), 1000); }); // resolve 运行 .then 中的第一个函数 promise.then( result => alert(result), // 1 秒后显示 "done!" error => alert(error) // 不运行 );

对 rejected 的 promise 做出反应:

let promise = new Promise(function(resolve, reject) { setTimeout(() => reject(new Error("Whoops!")), 1000); }); // reject 运行 .then 中的第二个函数 promise.then( result => alert(result), // 不运行 error => alert(error) // 1 秒后显示 "Error: Whoops!" );

如果只对成功完成感兴趣,可以只传一个函数参数:

let promise = new Promise(resolve => { setTimeout(() => resolve("done!"), 1000); }); promise.then(alert); // 1 秒后显示 "done!"

catch:只处理 error

如果只对 error 感兴趣,可以使用null作为第一个参数:.then(null, errorHandlingFunction);更简洁的写法是.catch(errorHandlingFunction),二者完全等价:

let promise = new Promise((resolve, reject) => { setTimeout(() => reject(new Error("Whoops!")), 1000); }); // .catch(f) 与 promise.then(null, f) 一样 promise.catch(alert); // 1 秒后显示 "Error: Whoops!"

清理:finally

就像try {...} catch {...}中的finally子句一样,promise 也有finally。调用.finally(f)类似于.then(f, f)——当 promise settled(无论 resolve 还是 reject)时f就会执行。它的典型用途是执行清理/终结操作:停止加载指示器、关闭不再需要的连接等。

new Promise((resolve, reject) => { /* 做一些需要时间的事,之后调用可能会 resolve 也可能会 reject */ }) // 在 promise 为 settled 时运行,无论成功与否 .finally(() => stop loading indicator) // 所以,加载指示器始终会在我们继续之前停止 .then(result => show result, err => show error)

但请注意:finally(f)并不完全是then(f, f)的别名,两者有重要区别:

  1. finally处理程序没有参数——在finally中我们不知道 promise 是成功还是失败,这没关系,因为它的任务通常是执行「常规」的完成程序(finalizing procedures);
  2. finally会把结果或 error 传递给下一个合适的处理程序。例如结果从finally传递给了then:
new Promise((resolve, reject) => { setTimeout(() => resolve("value"), 2000) }) .finally(() => alert("Promise ready")) // 先触发 .then(result => alert(result)); // <-- .then 显示 "value"

error 也会原样穿过finally抵达catch:

new Promise((resolve, reject) => { throw new Error("error"); }) .finally(() => alert("Promise ready")) // 先触发 .catch(err => alert(err)); // <-- .catch 显示这个 error
  1. finally处理程序不应返回任何内容——返回值会被默认忽略。唯一的例外是当finally处理程序抛出 error 时,这个 error(而非之前的结果)会转到下一个处理程序。

总结:

  • finally处理程序拿不到前一个处理程序的结果(它没有参数),结果被传递给了下一个合适的处理程序;
  • 如果finally返回了一些内容,这些内容会被忽略;
  • 当finally抛出 error 时,执行将转到最近的 error 处理程序。

只要把finally正确用于「常规清理」,这些特性都会非常有用。

附加处理程序:settled 之后也能订阅

如果 promise 处于 pending 状态,.then/catch/finally处理程序会等待它的结果;但有时候当我们添加处理程序时,promise 可能已经 settled 了——此时这些处理程序会立即执行:

// 这个 promise 在被创建后立即变为 resolved 状态 let promise = new Promise(resolve => resolve("done!")); promise.then(alert); // done!(立刻显示)

这使 promise 比现实生活中的「订阅列表」强大得多:如果歌手已经发布了单曲,之后才在订阅列表上注册的人很可能收不到通知——现实生活的订阅必须在活动开始前进行;而 Promise 则非常灵活,我们可以随时添加处理程序,如果结果已经存在,它们就会立刻执行。

实战重构:用 Promise 重写 loadScript

上一章(回调基础)中我们见过基于回调的loadScript函数,它通过script.onload/script.onerror通知结果:

function loadScript(src, callback) { let script = document.createElement('script'); script.src = src; script.onload = () => callback(null, script); script.onerror = () => callback(new Error(`Script load error for ${src}`)); document.head.append(script); }

现在用 Promise 重写它。新函数不再需要回调,而是创建并返回一个在加载完成时 resolve 的 promise 对象,外部代码用.then添加处理程序(订阅函数):

function loadScript(src) { return new Promise(function(resolve, reject) { let script = document.createElement('script'); script.src = src; script.onload = () => resolve(script); script.onerror = () => reject(new Error(`Script load error for ${src}`)); document.head.append(script); }); }

仓库中 head.html 保留了这份 Promise 版loadScript的完整实现,供配套练习页面直接引用。使用方法如下:

let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"); promise.then( script => alert(`${script.src} is loaded!`), error => alert(`Error: ${error.message}`) ); promise.then(script => alert('Another handler...'));

Promise 相对回调模式的优势

Promisecallback
Promise 允许我们按照自然顺序编码:先运行loadScript,再用.then处理结果。调用loadScript(script, callback)时,必须预先准备好callback函数——即在调用loadScript之前就要知道如何处理结果。
可以按需在 promise 上多次调用.then,每次调用都像在「订阅列表」中添加一个新的订阅函数(详见下一章 Promise 链)。只能有一个回调。

因此,promise 提供了更好的代码流与灵活性。更多内容将在 Promise 链 与 Promise 错误处理 中展开。

仓库配套练习:从原理到落地

本小节对应的三个练习任务覆盖了「单一结果」「无参 resolve」「异步 resolve 与 DOM 集成」三类典型场景,直接印证上文所述的 Promise 语义。

练习一:用 promise 重新解决?

任务(task.md):下面的代码会输出什么?

let promise = new Promise(function(resolve, reject) { resolve(1); setTimeout(() => resolve(2), 1000); }); promise.then(alert);

答案(solution.md):输出1。第二个resolve调用被忽略,因为只有第一次对reject/resolve的调用才会被处理,进一步的调用都会被忽略——这正是「单一结果」规则的实际验证。

练习二:基于 promise 的延时

任务(task.md):内建函数setTimeout使用回调,请创建基于 promise 的替代方案——函数delay(ms)返回一个 promise,在ms毫秒后 resolve:

function delay(ms) { // 你的代码 } delay(3000).then(() => alert('runs after 3 seconds'));

参考实现(solution.md):

function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } delay(3000).then(() => alert('runs after 3 seconds'));

注意:此任务中resolve是不带参数调用的——我们不从delay中返回任何值,只是确保延迟即可。这也印证了「resolve/reject只需要一个参数(或不包含任何参数)」的规则。

练习三:带有 promise 的圆形动画

任务(task.md)要求重写 CSS 动画章节 animate-circle-callback 任务 的解决方案中的showCircle函数,使其返回 promise 而非接受回调,新用法如下:

showCircle(150, 150, 100).then(div => { div.classList.add('message-ball'); div.append("Hello, world!"); });

仓库配套页面 solution.view/index.html 给出了完整实现。核心思路是:promise 在transitionend事件触发时才 resolve,从而把「动画完成」这一异步时刻暴露给.then:

function showCircle(cx, cy, radius) { let div = document.createElement('div'); div.style.width = 0; div.style.height = 0; div.style.left = cx + 'px'; div.style.top = cy + 'px'; div.className = 'circle'; document.body.append(div); return new Promise(resolve => { setTimeout(() => { div.style.width = radius * 2 + 'px'; div.style.height = radius * 2 + 'px'; div.addEventListener('transitionend', function handler() { div.removeEventListener('transitionend', handler); resolve(div); }); }, 0); }); }

这个例子展示了 Promise 更进阶的用法:executor 内部可以组合setTimeout与 DOM 事件,把任意异步时点(这里是 CSS 过渡动画结束)封装成 promise 的 resolve 时机,从而让调用方用统一的.then风格消费各种异步结果。

小结

本篇文章完整覆盖了 Promise 基础的全部核心内容:

  • 生产者/消费者模型:Promise 是连接耗时「生产者代码」与「消费者代码」的订阅式对象;
  • 构造器与 executor:new Promise(executor)创建时 executor 自动运行,成功后调用resolve(value)、失败后调用reject(error);
  • 内部状态:state从"pending"变为"fulfilled"/"rejected",result从undefined变为value/error,且只能迁移一次;
  • 消费方法:.then同时注册成功/失败处理,.catch等价于.then(null, f),.finally用于无参数的常规清理并透传结果;
  • 实战能力:通过loadScript的重构对比了 promise 与 callback 的代码流差异,并通过仓库配套的三个练习验证了「单一结果」「无参 resolve」「异步 resolve」等关键语义。

下一章 Promise 链 将介绍如何在多个异步步骤之间串联 promise,请继续阅读。

  • 文档
  • 教程
  • 前端

【免费下载链接】zh.javascript.info

现代 JavaScript 教程(The Modern JavaScript Tutorial),以最新的 ECMAScript 规范为基准,通过简单但足够详细的内容,为你讲解从基础到高阶的 JavaScript 相关知识。

项目地址:https://gitcode.com/gh_mirrors/zh/zh.javascript.info
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/23 9:44:12

如何用AI4AnimationPy的FABRIK求解器实现实时逆运动学:完全指南

如何用AI4AnimationPy的FABRIK求解器实现实时逆运动学&#xff1a;完全指南 【免费下载链接】ai4animationpy A Python framework for AI-driven character animation using neural networks. 项目地址: https://gitcode.com/gh_mirrors/ai/ai4animationpy AI4Animation…

作者头像 李华
网站建设 2026/10/21 5:11:32

大模型上下文管理实战:context-mode 设计与 token 优化方案

1. 项目概述&#xff1a;context-mode 到底是什么1.1 从一次翻车现场说起先讲一个我亲身经历的场景。半年前我在做一个智能客服机器人&#xff0c;用户和机器人聊了大概二十轮之后&#xff0c;机器人开始"失忆"——一会儿说"刚才您说要退款&#xff0c;请问订单…

作者头像 李华
网站建设 2026/10/21 16:52:14

工业级电源路径保护:TPS259483+STM32G4构建嵌入式韧性供电系统

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/21 1:51:14

TPS259483AYWPR+STM32F303RE工业电源路径保护方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/23 13:37:34

零配置评论组件:为静态手账博客接入轻量无侵入的 Utterances 评论区

十月七日的午后&#xff0c;阳光透过阳台的白纱窗&#xff0c;在木地板上洒下一片片斑驳的光影。 我正在浏览自己刚搭好的静态手账博客&#xff0c;读到一篇前几天写的关于“水彩界面微卷阴影”的技术随笔。文章末尾空空荡荡&#xff0c;只有一行简单的版权声明。有几位常常一起…

作者头像 李华