编程语言研究书库

回调 — 异步的原始写法,和它欠下的三笔债

回调 — 异步的原始写法,和它欠下的三笔债

这一章回答两个问题: 回调这个"传一个函数过去、完了叫我"的写法具体怎么写才对; 以及为什么它迟早会把代码拖进"回调地狱",走出来有哪几条路。 读完你会写出规范的回调代码,更重要的是:能判断一段回调代码该不该重构、往哪个方向重构。

1. 这一章讲什么

第 01 章说异步回调会进队列排队,这一章落到人的一侧:写代码的人怎么把"办完叫我"这件事安排好。结构是:

  1. 回调的四种写法(第 3 节);
  2. 错误优先约定——回调时代的错误处理标准(第 4 节);
  3. 嵌套失控形成的回调地狱,和它到底坏在哪(第 5 节);
  4. 四条解围路线,逐条走到代码(第 6 节)。

在全书链条里,这是"问题章":后面 Promise(第 03 章)和 async/await(第 05 章)的每一项设计,都能在本章的某个痛点上找到出处。

2. 顶层全景:餐厅点单的比方

原书的比方:你在餐厅点单,点完回到座位上等,而不是堵在出餐口;菜做好了,服务员端过来——这个"端过来"的动作,就是回调被执行1。

写成代码结构:

  你:     fetchData(processData)
              │ ① 启动耗时操作,把 processData 递过去
              ▼
  厨房:   (2 秒后)数据好了
              │ ② 回头调用 processData(数据)
              ▼
  你:     processData 里写"收到数据后干什么"

回调的本质分工:发起方不等待,收尾方在另一个时间点运行。 这两半之间唯一的连接,是那个被传来传去的函数。

3. 核心原理(一):四种写法,从随手到规范

原书从简单到规范给了四种形态,全部围绕同一个 fetchData(callback) 骨架2。

写法一:匿名函数就地写。 只用一次、逻辑短小时最顺手:

greet("Alice", function() {
  console.log("Greeting completed!");
});

写法二:具名函数。 回调变复杂后,先定义再传名,代码自带说明书:

function afterGreeting() { console.log("Greeting completed!"); }
greet("Bob", afterGreeting);

写法三:带数据回传。 异步操作把结果当数据喂给回调:

function fetchUserData(userId, callback) {
  setTimeout(() => {
    const user = { id: userId, name: "Jane Doe", email: "jane@example.com" };
    callback(user);          // 结果从这一行流进回调
  }, 1000);
}
fetchUserData(123, displayUserInfo);

写法四:错误优先。 这是 Node.js 时代定下的行业约定,也是本章的承重写法:回调的第一个参数(函数被调用时按位置传进来的第一个值)永远留给错误;没有错误就传 null3。

function readFile(filename, callback) {
  setTimeout(() => {
    if (filename === "nonexistent.txt") {
      callback(new Error("File not found"));   // 有错误:第一个参数是错误
    } else {
      callback(null, "File contents");          // 没错误:第一个参数是 null
    }
  }, 1000);
}
function handleFileContents(error, contents) {
  if (error) { console.error("Error reading file:", error.message); }
  else       { console.log("File contents:", contents); }
}

消费端永远是同一个形状:if (error) 处理错误; else 用结果。这个约定解决的是"失败怎么传回去"——同步代码里失败靠抛异常,异步回调里没人能接住抛出的异常(调用它的栈早没了),所以只能把错误当数据传回来。

4. 主线例子:取用户 → 取帖子 → 取评论

本章主线例子沿用第 00 章的天气主线思路,换成三段式数据链(各步耗时是为演示编的):输入用户 ID 123,先取用户资料(1 秒),再取该用户的帖子(1 秒),再取第一篇帖子的评论(1 秒)。每一步都是"错误优先"回调:

  输入:userId = 123
    │ getUserData(123, cb)          1 秒后 → cb(null, {id:123, name:"Alice"})
    ▼
  拿到 user → getUserPosts(123, cb) 1 秒后 → cb(null, [{id:1,...},{id:2,...}])
    ▼
  拿到 posts → getPostComments(1, cb) 1 秒后 → cb(null, [{id:101,text:"Great post!"}])
    ▼
  三段齐全,展示

每一步都可能失败(比如第二步返回 "User not found"),失败时 cb(error) 被调用,链就断在该处。

单步看,这个模式干净利落。三步串起来,问题就来了——下一步必须写在上一层的回调里(因为结果只有到那时才存在),于是代码开始向右缩进。

5. 核心原理(二):回调地狱,以及它的三笔账

把第 4 节的三步按唯一可能的写法嵌套起来4:

getUserData(123, (error, user) => {
  if (error) { console.error("Error fetching user data:", error); return; }
  console.log("User:", user);

  getUserPosts(user.id, (error, posts) => {
    if (error) { console.error("Error fetching user posts:", error); return; }
    console.log("Posts:", posts);

    getPostComments(posts[0].id, (error, comments) => {
      if (error) { console.error("Error fetching post comments:", error); return; }
      console.log("Comments:", comments);
      // 还有第四步的话,继续往右缩……
    });
  });
});

三层不算最坏,但结构问题已经全部显形。原书把账一笔一笔列得很清楚5:

账坏在哪
可读性嵌套越深代码越靠右,逻辑流要从屏幕左边追到右边再追回来
错误处理每层都要重写一遍 if (error),漏一层就是静默失败
作用域外层的名字要靠闭包才能被内层看见,改名字、重构都提心吊胆
维护加一步、调顺序,得在多层缩进里动刀,极易引入 bug
并行难这种写法天生是串行的;想让互不依赖的两步同时跑,得另起炉灶
调试执行流在屏幕上不连续,断点打下去不知道会停在哪个缩进层

注意原书点出的一件事:这不只是美观问题,它直接吃掉了开发时间、 bug 数量和新人的上手速度——是生产力问题6。

6. 四条解围路线

原书给了四条,力度递增。前三条还在回调的框架里收拾,第四条直接换框架。

6.1 路线一:抽成具名函数(展平缩进)

把每一层回调提成顶层具名函数,互相传递名字而不是嵌套定义7:

function handleUserData(error, user) {
  if (error) { console.error(...); return; }
  console.log("User:", user);
  getUserPosts(user.id, handleUserPosts);
}
function handleUserPosts(error, posts) {
  if (error) { console.error(...); return; }
  console.log("Posts:", posts);
  getPostComments(posts[0].id, handlePostComments);
}
function handlePostComments(error, comments) {
  if (error) { console.error(...); return; }
  console.log("Comments:", comments);
}
getUserData(123, handleUserData);

**解决了:**缩进和可读性。**没解决:**错误分支还是每层手写;流程被切成一串函数,顺序要看函数之间的传递关系才能拼出来。

6.2 路线二:模块化(把"怎么取"与"怎么编排"分开)

把"取数据并组对"包成小函数,主流程只负责串8。原书把"用户+帖子"合成 fetchUserDataAndPosts、"评论"单独成 fetchPostComments,主函数 processUserInfo 只做编排。这是后面第 11 章"分离关注点"最佳实践的雏形。

6.3 路线三:控制流库(如 async.js)

库里有人把常见编排模式写好了。原书用 async.waterfall 改写主线例子9:

const async = require('async');
async.waterfall([
  (callback) => getUserData(123, callback),
  (user, callback) => getUserPosts(user.id, (error, posts) => callback(error, user, posts)),
  (user, posts, callback) => getPostComments(posts[0].id, (error, comments) => callback(error, user, posts, comments))
], (error, user, posts, comments) => {
  if (error) { console.error("Error:", error); return; }
  console.log("User:", user); console.log("Posts:", posts); console.log("Comments:", comments);
});

waterfall(瀑布)的意思是:上一步的结果顺着管道流进下一步,最后统一收尾。任务列表是一列,不再是金字塔。这是"用数据结构描述流程"的思路——和第 01 章的队列思想同宗:别用嵌套表达顺序,用排队表达顺序。

判断(我们的,不是书里的): 这条路线今天已经很少直接用了(它的位置被 Promise 链和 async/await 取代),但它值得记住,因为 waterfall 的思想没有死——Promise 链本质上是内置在语言里的 waterfall,只是管道里流的是 Promise 而不是 (error, ...values)。 如果错,会错在: 某些 Node 风格的旧代码库(以及某些重回调的库接口)里 async.js 仍然常见;说"很少用"对入门读者够用,对维护老系统的人偏乐观。

6.4 路线四:换代——Promise 与 async/await

原书用两小段预览了换代后的样子,细节留给第 03、05 章10。同一条主线用 Promise:

getUserData(123)
  .then(user => { console.log("User:", user); return getUserPosts(user.id); })
  .then(posts => { console.log("Posts:", posts); return getPostComments(posts[0].id); })
  .then(comments => { console.log("Comments:", comments); })
  .catch(error => console.error("Error:", error));

再换成 async/await,代码读起来就是第 4 节那张流程图本身。对比之下结论自然浮现:回调地狱不是"回调"的罪,是"用嵌套表达顺序"的罪;Promise 和 async/await 只是把顺序还给了直线代码。

6.5 主线例子补完:成功路径与失败路径各走一遍

第 4 节只给了正常顺序。把两条路径都走完,回调的形状才完整(耗时是为演示编的)。

成功路径(三步都成):t=0 发起取用户;t=1 秒用户数据回来,callback(null, user) 被调用——注意第一个参数是 null,这是错误优先约定的"没有错误"信号;t=1 秒这一刻第二步才发出;t=2 秒帖子回来;t=3 秒评论回来。总耗时 3 秒,三次 if (error) 检查全部走 else 分支。

失败路径(第二步失败):t=0 发起取用户;t=1 秒用户回来,进第二层;t=1 秒发出取帖子;假设服务器返回"User not found",callback(error) 被调用——从这一刻起,整个金字塔的下半部分(取评论那层)永远不会被建出来,因为下一步的启动代码写在上一步的成功分支里。错误优先约定的巧妙正在这里:失败天然带着"别再往下走"的含义,只要调用方记得检查。

两条路径对比,能看出回调时代错误处理的真正成本:成功路径的缩进有多深,失败路径的检查就得写多少份。第 03 章的 Promise 链把这两份成本合并成一份链尾 catch,就是这个对比的直接答案。

6.6 控制权易手:三个具体的风险

第 00 章点了"控制权易手"的名字,这里把它展开成回调时代人人担心、却没有语言级防护的三件事11:

风险场景回调时代的土办法
多调你传的成功回调被调了两次,一笔转账扣了两回自己在回调里立"已调过"的旗标
少调出错时对方既不调成功也不调失败,你的程序永远等自己另设超时器兜底
乱调先调成功又调失败,两个分支都执行了自己检查状态,后面的调用忽略

Promise 把这三件事收进了对象内部:状态只能迁移一次(第 03 章的不可变性)直接消灭了"多调"和"乱调";落定之后任何时刻挂 .then/.catch 都能拿到结果,配合超时模式(第 13 章的 withTimeout)能兜住"少调"。回看进化链:Promise 的第一笔债偿还,其实还清了这三笔暗账。

6.7 今天还在哪里写回调

学完本章不该得出的结论是"回调已死"。三种场景它仍是标准写法12:

  1. 事件监听:button.addEventListener('click', handler) 的 handler 就是回调——事件每次来都要调一次,Promise"只落定一次"的模型反而不合适;
  2. Node.js 老接口:大量以回调为最后参数的存量函数(读文件、连数据库的老版本封装),日常维护躲不开;
  3. 定时器与动画:setTimeout、requestAnimationFrame 的接口天生就是回调形状。

后两种常见做法是"包一层":在回调里调用 resolve/reject,把它转成 Promise(第 03 章 fetchUserData 的构造、第 06 章的 delay 函数都是这个手法的实例)。判断标准:这个回调会被调多次吗?会——留着回调;只调一次——包成票。

6.8 模块化路线逐步走查

6.2 说得太快,这里把原书的模块化重构逐层走一遍13。目标不变:取用户 → 取帖子 → 取评论,最后合成一个对象。重构分三层函数:

function fetchUserDataAndPosts(userId, callback) {
  getUserData(userId, (error, user) => {
    if (error) { callback(error); return; }
    getUserPosts(user.id, (error, posts) => {
      if (error) { callback(error); return; }
      callback(null, { user, posts });        // 第一层出口:用户+帖子打包
    });
  });
}
function fetchPostComments(postId, callback) {
  getPostComments(postId, (error, comments) => {
    if (error) { callback(error); return; }
    callback(null, comments);                  // 第二层出口:评论
  });
}
function processUserInfo(userId, callback) {
  fetchUserDataAndPosts(userId, (error, result) => {
    if (error) { callback(error); return; }
    fetchPostComments(result.posts[0].id, (error, comments) => {
      if (error) { callback(error); return; }
      callback(null, { user: result.user, posts: result.posts, comments });
    });
  });
}

三层各自的分工:前两层把"原始回调接口"翻译成"带错误优先约定的复合操作"——它们的内部仍然是嵌套的,但嵌套被关进了小盒子里;第三层只看见两个干净的盒子,编排读起来像顺序。注意每层 callback 的调用形状完全一致(有错传错、无错传 null+数据),这正是错误优先约定作为"接口语言"的价值:三层函数不需要互相认识,凭约定就能拼起来。

但别忽略代价:错误检查的份数没有减少,只是分散到了各层。每层的 if (error) 还在,加起来比金字塔版只多不少。模块化治"读",不治"写"——治"写"要靠下一代机制(Promise 把每层的 if(error) 变成链尾一个 catch,第 03 章)和库(6.3 的 waterfall 把传递也标准化)。

6.9 waterfall 的参数流:每一步多吃一个、多吐一个

async.waterfall 读起来像数组,但它的参数流有个具体形状,值得画出来14:

  步骤1: (callback)        → 调 getUserData(123, callback)
                              成功时 callback(null, user) ──→ 步骤2 收到 user
  步骤2: (user, callback)  → 调 getUserPosts(user.id, (error, posts) =>
                              callback(error, user, posts))   ──→ 步骤3 收到 user, posts
  步骤3: (user, posts, callback) → 调 getPostComments(posts[0].id, (error, comments) =>
                              callback(error, user, posts, comments)) ──→ 尾回调收到全部四样
  尾回调: (error, user, posts, comments)

两个细节。其一,步骤2 必须把 user 透传下去——waterfall 不会替你记,上一步的产出你不放进 callback,后面就永远拿不到;原书的写法是在包一层回调里手动 callback(error, user, posts)。其二,任何一步 callback(error),流直接跳到尾回调,中间步骤全部跳过——错误短路是结构白送的,不用每层写(但每层还是写了 if(error),这是回调时代的宿命)。

对照 6.8 的手写模块化:waterfall 省掉了"盒子之间的接线"(谁调谁、传什么),但没有省掉错误检查;Promise 链连错误检查都收走了。三条路线的对比表就是本章的结论:

嵌套金字塔模块化(6.8)waterfall(6.3)Promise(6.4)
读灾难好好好
错误检查份数N 份N 份N 份1 份
接线(传参)缩进手写库管链管
顺序的表达缩进函数名数组return

6.10 本章主线例子的输出时间线

6.5 走了成功与失败两条路径,把成功路径再压成一张时间线,和 6.4 的 Promise 版对照(耗时为演示编,各请求 1 秒):

  回调版:                            Promise 版:
  t=0.0 getUserData 发出              t=0.0 getUserData 发起,票造好
  t=1.0 user 回来 → then 无           t=1.0 票兑现 → .then 收到 user
  t=1.0 getUserPosts 发出(嵌套里)    t=1.0 return 下一张票 → 自动发出
  t=2.0 posts 回来                    t=2.0 第二张票兑现 → 下一个 .then
  t=2.0 getPostComments 发出          t=2.0 return 第三张票 → 自动发出
  t=3.0 comments 回来,展示            t=3.0 第三张票兑现,展示

总耗时一样,都是 3 秒——换代不换速度,换的是形状。回调版的每一步"发出下一步"埋在上一层的回调里;Promise 版由链自动衔接(return 票即自动发下一步),人的代码里只剩"处理当前值"。这正是 6.9 那张结论表最后一行的含义:顺序的表达权从缩进手里收回来,交给 return。

6.11 什么时候不要重构

最后泼一盆冷水:四条解围路线不是见到嵌套就上。两层回调、逻辑短小、只此一处——保留嵌套反而最诚实,重构后的抽象(多一个函数、多一层间接)比这层缩进更贵。顺带一提:重构里的"内层函数看得见外层名字"在 JavaScript 里有个专门的名字——闭包(定义在别的函数内部、因此一直能看见外层名字的函数)。本章的嵌套回调全是闭包,6.8 模块化的内层回调也是;它既是被批评的缩进的来源,也是"外层数据能被内层使用"的机制本身。第 12 章还会见到它在内存泄漏里的另一面。

判断线有两条:这套嵌套会不会长(步骤还会加吗?会——趁早展平);这套逻辑会不会被复用(会——模块化或换代)。都不沾,就让金字塔待着——重构是买未来,不是买体面,这笔账每次动手前都要重算一遍。

6.12 错误优先约定的三个微细节

写法四有三个容易忽略的微细节,补上才谈得上"会写回调"15:

其一,null 不是"省略"。 成功时传的是 callback(null, data)——第一个参数显式给 null,而不是少传一个参数。因为少传会让 data 滑进第一个位置,消费端 if (error) 会把数据当错误处理。位置的稳定性是这个约定的全部意义。

其二,错误是对象,不是字符串。 原书的例子里失败传的是 new Error("File not found")。传字符串也能跑,但会丢掉错误对象携带的栈信息(第 07 章 5.1 要靠它定位),消费端也没法分辨错误的种类。第 04 章纪律四的自定义错误类型,就是这条细节的延伸。

其三,检查必须第一行就做。 消费端的标准形状 if (error) {...return} else {...} 里,任何放在检查之前的逻辑都会在错误场景下空转甚至崩掉。约定靠"零例外"才成为约定——第 06 章的分诊重试(4xx 不重试)也是同一个精神在 Promise 时代的延续。

7. 作者的判断与证据

本章的判断都押在一个权衡上:回调仍然必须学,因为定时器、事件监听、Node 风格的老接口到处都是回调;但新代码不该再用嵌套回调编排流程16。证据是前面对比代码——同一需求五种写法摆在一起,可读性差距不需要论证。

本章还有一个未言明的判断:作者把"错误优先约定"当作 unquestioned 的行业标准直接教,没有讨论它的替代方案——这符合入门书的定位,但读者要知道,这个约定是 Node.js 社区 2010 年代初的遗产,浏览器侧从未强制过,两种生态的库今天仍然混用。读老代码时先找"回调的第一个参数是什么形状",一眼就能判断它出身哪一派。

"错误优先约定"被当作标准写法直接教,没有讨论它的争议(比如回调被调两次怎么办)——这是入门书的合理沉默,但要知道沉默存在。

8. 边界与局限

  • 没讲回调的"信任问题"(控制权易手的全部后果:调用时机、次数、参数)的防御写法,只在第 00 章点了名字;
  • 控制流库只演示了 waterfall,串行/并行的其他组合(map、series、parallel)没有展开;
  • 回调与 Promise 混用时的接口转换(手动包一层 Promise)要到第 03 章才见到。

9. 可带走的

  1. 回调 = 传给别人的函数,时机到了被调用;发起方不等待,收尾在另一个时间点运行;
  2. 错误优先约定:回调第一个参数留给出错,正常时传 null——消费端永远是 if (error) else;
  3. 回调地狱的账是六笔:可读性、错误分支、作用域、维护、并行难、调试;前三笔最疼;
  4. 解围力度递增:具名函数 < 模块化 < 控制流库 < 换代 Promise;前两治表,第三第四治本;
  5. async.waterfall 的思想(用一列任务表达流程)活在今天的 Promise 链里;
  6. 别用嵌套表达顺序——这句话是整个异步进化史的压缩版。

10. 原文地图

主题原书章原文位置
回调定义、餐厅比方Chapter 2: Callbackstext/05-ch02-chapter-2-callbacks.txt:9(搜「passed as an argument」) · text/05-ch02-chapter-2-callbacks.txt:15(搜「waiter」)
fetchData 示例与输出同上text/05-ch02-chapter-2-callbacks.txt:21(搜「fetchData」)
匿名/具名/带参回调同上text/05-ch02-chapter-2-callbacks.txt:53(搜「Anonymous」) · text/05-ch02-chapter-2-callbacks.txt:68(搜「Named」) · text/05-ch02-chapter-2-callbacks.txt:85(搜「Parameters」)
错误优先约定同上text/05-ch02-chapter-2-callbacks.txt:105(搜「error-first」)
回调地狱示例同上text/05-ch02-chapter-2-callbacks.txt:135(搜「Callback Hell」) · text/05-ch02-chapter-2-callbacks.txt:170(搜「getUserData」)
六笔账同上text/05-ch02-chapter-2-callbacks.txt:200(搜「Readability」) · text/05-ch02-chapter-2-callbacks.txt:212(搜「not just about aesthetics」)
具名函数重构同上text/05-ch02-chapter-2-callbacks.txt:220(搜「Named Functions」)
模块化同上text/05-ch02-chapter-2-callbacks.txt:254(搜「Modularization」)
async.waterfall同上text/05-ch02-chapter-2-callbacks.txt:336(搜「waterfall」)
Promise/await 预览同上text/05-ch02-chapter-2-callbacks.txt:340(搜「next chapter」) · text/05-ch02-chapter-2-callbacks.txt:416(搜「looks almost like synchronous」)

本章教的是"旧世界"的完整地形——读老代码、维护旧接口都离不开它;而下一章的每一个设计决策,都能在本章的某张账上找到编号。

Footnotes

  1. 出处:「Chapter 2: Callbacks」第 15 段(text/05-ch02-chapter-2-callbacks.txt:15,搜「waiter」)。餐厅比方:点单后回座位,餐好由服务员送来,"The act of the waiter bringing your food… is analogous to a callback function being executed"。 ↩

  2. 出处:同章第 53-101 段(text/05-ch02-chapter-2-callbacks.txt:53,搜「Anonymous」;:68,搜「Named」;:85,搜「Parameters」)。三种写法的小节标题依次为 Anonymous Functions、Named Functions、Callbacks with Parameters。 ↩

  3. 出处:同章第 105 段(text/05-ch02-chapter-2-callbacks.txt:105,搜「error-first」)。原文:"a common pattern in Node.js… is to use an error-first callback style, where the first parameter of the callback is reserved for an error object." ↩

  4. 出处:同章第 170-194 段(text/05-ch02-chapter-2-callbacks.txt:170,搜「getUserData」)。嵌套例子的三个函数与回调结构照原书。 ↩

  5. 出处:同章第 198-210 段(text/05-ch02-chapter-2-callbacks.txt:200,搜「Readability」;:202,搜「Error Handling」;:204,搜「Limited Scope」;:206,搜「Difficult to Maintain」;:208,搜「Sequential Execution」;:210,搜「Debugging」)。六个问题逐条列出。 ↩

  6. 出处:同章第 212 段(text/05-ch02-chapter-2-callbacks.txt:212,搜「not just about aesthetics」)。原文:"not just about aesthetics or code style; it significantly impacts the maintainability, scalability, and reliability"。 ↩

  7. 出处:同章第 224-252 段(text/05-ch02-chapter-2-callbacks.txt:224,搜「handleUserData」)。具名函数重构代码照原书,"we've flattened the structure of our code"。 ↩

  8. 出处:同章第 258-312 段(text/05-ch02-chapter-2-callbacks.txt:258,搜「fetchUserDataAndPosts」)。模块化重构代码照原书。 ↩

  9. 出处:同章第 320-336 段(text/05-ch02-chapter-2-callbacks.txt:322,搜「waterfall」)。原文:"async.waterfall function allows us to define a series of asynchronous operations that depend on each other's results"。 ↩

  10. 出处:同章第 342-416 段(text/05-ch02-chapter-2-callbacks.txt:393,搜「eliminating the deep nesting」;:416,搜「looks almost like synchronous」)。Promise 版"eliminating the deep nesting associated with callbacks",async/await 版"looks almost like synchronous code"。 ↩

  11. 出处:「Chapter 2: Callbacks」第 9-17 段(text/05-ch02-chapter-2-callbacks.txt:9,搜「passed as an argument」)。回调定义与"executed after the completion of that function"的语义;三类风险是对第 00 章"inversion of control"的展开(「Introduction」第 95 段,text/03-fm-introduction.txt:95,搜「Inversion of Control」)。 ↩

  12. 出处:「Chapter 2: Callbacks」第 418 段(text/05-ch02-chapter-2-callbacks.txt:418,搜「remain an important concept」)。原文:"callbacks remain an important concept in JavaScript";事件与定时器的回调接口形态见「Introduction」第 82、281 段(text/03-fm-introduction.txt:82,搜「setTimeout」;:281,搜「setTimeout and setInterval」)。 ↩

  13. 出处:「Chapter 2: Callbacks」第 258-312 段(text/05-ch02-chapter-2-callbacks.txt:258,搜「fetchUserDataAndPosts」;:274,搜「fetchPostComments」;:284,搜「processUserInfo」)。三层函数照原书。 ↩

  14. 出处:「Chapter 2: Callbacks」第 320-336 段(text/05-ch02-chapter-2-callbacks.txt:322,搜「waterfall」)。waterfall 任务数组与尾回调照原书。 ↩

  15. 出处:「Chapter 2: Callbacks」第 105-131 段(text/05-ch02-chapter-2-callbacks.txt:105,搜「error-first」;:111,搜「new Error」)。错误优先约定、new Error 对象与 handleFileContents 的消费形状照原书。 ↩

  16. 出处:同章第 418 段(text/05-ch02-chapter-2-callbacks.txt:418,搜「remain an important concept」)。原文:"callbacks remain an important concept in JavaScript",同时强调要掌握 Promise 与 async/await 这些新模式。 ↩

On this page