Callback 函数在 JavaScript 编程中非常常见,这和 JavaScript 本身“事件驱动 + 单线程 + 大量异步”的特性密切相关。

简单说一句:所谓 callback,本质上就是把一个函数当作参数传给另一个函数,在某个合适的时机再把它“叫出来”执行。

理解了这件事,其它内容就好办多了。


callback 基础

先看一个非常简单的例子:

const x = function () {
  console.log("I am called from inside a function");
};

const y = function (callback) {
  console.log("do something...");
  callback();
};

y(x);

在上面的例子中,x 被作为参数传给了 y,并且在 y 的内部被调用。这就是一个典型的 callback 使用场景:把“要做什么”交给别的函数来决定,自己只负责“在什么时候做”

callback 的一个直接好处,就是能让代码变得更灵活。

先看一个不使用 callback 的版本:

const calc = function (num1, num2, calcType) {
  if (calcType === "add") {
    return num1 + num2;
  } else if (calcType === "multiply") {
    return num1 * num2;
  }
};

console.log(calc(2, 3, "add")); // 5
console.log(calc(2, 3, "multiply")); // 6

这个写法当然可以工作,但扩展起来会有点麻烦,每新增一种计算方式(减法、除法、取模……),都得在 calc 里继续加 if / else if 分支。

换成 callback 之后,思路就不一样了——calc 不关心“你要算什么”,它只负责“帮你调用”:

const add = function (a, b) {
  return a + b;
};

const multiply = function (a, b) {
  return a * b;
};

const calc = function (num1, num2, callback) {
  return callback(num1, num2);
};

console.log(calc(2, 3, add)); // 5

const printNum = function (a, b) {
  console.log(`There are two numbers ${a} and ${b}`);
};

calc(2, 3, printNum);

现在,calc 成了一个通用的“执行器”, 具体要算什么、要打印什么,全由传进来的 callback 决定。这样不仅保留了原有的功能,还给了调用者很大的发挥空间——想扩展逻辑,写一个新的函数丢进去就行。


在 Node.js 中的使用

在 Node.js 里,callback 基本可以算是“标配写法”。原因也很简单: JavaScript 是非阻塞(Non-blocking)的,当你发一个 HTTP 请求,或者访问数据库时,这些操作通常是异步的。主线程不会傻等,而是继续往下跑。

那么 等请求“跑完”之后,要做的事情放哪里呢?

答案就是:放在 callback 里。

下面用一个简单的“模拟访问数据库”的例子来说明:

function getUserFromDB(callback) {
  setTimeout(
    () =>
      callback({
        firstName: "Lane",
        lastName: "Tang",
      }),
    1000,
  );
}

function greetUser() {
  getUserFromDB(function (userObject) {
    console.log("Hi " + userObject.firstName);
  });
}

greetUser();

这里 getUserFromDB 并不会立刻返回用户数据,而是通过 setTimeout 模拟了一次耗时 1 秒的异步操作。 当“查库”结束后,它会调用传入的 callback,把 userObject 作为参数传进去。

从调用者的角度看,逻辑就变成了:

“先去帮我查一下用户,查完之后再调用我给你的这个函数,在里面打个招呼。”

这种模式在 Node.js 的各种 API 中比比皆是,文件读写、网络请求、数据库操作……都经常会看到 xxx(args, callback) 这样的签名。


callback hell 与 Promise 的问题

callback 非常强大,但当嵌套层级越来越多时,代码就容易变成经典的“回调地狱”(callback hell):一层套一层、缩进越来越深、错误处理四处飞,读起来让人头皮发麻。

为了解决这种可读性问题,JavaScript 社区逐渐引入了 Promise,后来又有了 async/await。它们的目标不是替代 callback,而是让异步逻辑以更接近同步代码的方式写出来,从而摆脱“地狱式缩进”。