讲解

回调函数是「交给别人、等合适时机再被调用」的函数。因为 JavaScript 里函数是一等公民,把它作为参数传给另一个函数是天经地义的事:数组方法 arr.map(fn) 里的 fn 是同步回调(立即、逐个被调用);setTimeout(fn, 1000)、事件监听 addEventListener('click', fn) 里的 fn 是异步回调(未来某个时刻被调用)。前端开发的本质很大程度上就是注册一堆回调,等待用户操作和数据到达。

Node.js 确立了「错误优先回调」的约定:回调的第一个参数是错误对象(没错则为 null),后续参数才是结果——fs.readFile(path, (err, data) => ...) 是这个风格的鼻祖。这个约定影响了整整一代异步 API 的设计。回调的问题在多层异步串联时暴露无遗:A 完成后调 B、B 完成后调 C,代码缩进不断右移,形成「回调地狱」,错误处理还要每层重复写,可读性和可维护性急剧恶化。

回调地狱催生了 Promise(下一章),Promise 又催生了 async/await。今天写异步流程首选 async/await,但回调并没有消失:事件监听、数组方法、大量第三方库的 API 仍然是回调风格。理解回调是理解整个异步体系的起点,特别是「注册时机」和「执行时机」分离这个核心心智模型。

示例

// 同步回调:立即被 map 逐个调用
const nums = [1, 2, 3];
const doubled = nums.map(function double(n) {
  return n * 2; // 这个回调在 map 内部被同步调用
});
console.log('同步回调:', doubled);

// 自己设计接收回调的函数
function retry(times, task, onDone) {
  let result;
  for (let i = 0; i < times; i++) {
    result = task(i);
  }
  onDone(result); // 完成后通知调用方
}
retry(3, (i) => '第 ' + (i + 1) + ' 次执行', (final) => {
  console.log('回调收到:', final);
});

// 错误优先风格(Node 约定)的同步模拟
function fakeReadFile(shouldFail, callback) {
  if (shouldFail) {
    callback(new Error('文件不存在'), null);
  } else {
    callback(null, '文件内容');
  }
}
fakeReadFile(false, (err, data) => {
  if (err) {
    console.log('出错:', err.message);
    return;
  }
  console.log('读到:', data);
});

异步回调(setTimeout、事件)需要浏览器或定时器环境,运行沙盒里看不到输出,示意如下:

// 异步回调:1 秒后才执行
setTimeout(() => {
  console.log('这条消息 1 秒后才出现');
}, 1000);

// 回调地狱:三层异步嵌套,缩进失控
loadUser(id, (err, user) => {
  if (err) return handle(err);
  loadOrders(user.id, (err, orders) => {
    if (err) return handle(err);
    loadOrderDetail(orders[0].id, (err, detail) => {
      if (err) return handle(err);
      render(detail); // 真正的逻辑被埋在最深处
    });
  });
});

常见坑

  • 把回调当成立即执行:onClick(fn()) 把函数调用结果传了进去,正确是 onClick(fn) 传函数本身——「传函数,别调用」。
  • 回调里丢失 this:普通函数作回调时 this 是默认绑定,需要外层 this 时用箭头函数或 bind。
  • 在回调里 return 想影响外层:回调的返回值是给调用它的内部代码的,外层函数早就返回了,return 到不了外层。
  • 回调地狱还逐层重复错误处理:每层 if (err) 复制粘贴。两层以上异步串联就该换 Promise/async-await。

小结

回调是把函数交出去延迟调用,分同步与异步;Node 的错误优先约定影响深远;多层嵌套形成回调地狱,解法在后面两章。下一章:Promise。