源本科技 | 码上会

JavaScript 回调函数

2026/03/13
22
0

学习目标

  • 掌握回调函数的核心定义、执行时机及其在 JavaScript 中的传递方式。

  • 理解回调函数在异步操作(如定时器、API 请求)和事件处理中的关键作用。

  • 识别“回调地狱”(Callback Hell) 的形成原因及其对代码可维护性的影响。

  • 了解现代异步解决方案(Promises 和 Async/Await)如何优化传统的回调模式。


什么是回调函数

在 JavaScript 中,回调函数 (Callback Function) 是指作为参数传递给另一个函数,并在稍后被执行的函数。

核心特性

  • 作为参数传递: 函数可以接收另一个函数作为参数。

  • 延迟执行: 回调函数允许一个函数在特定时间点或特定条件满足后调用另一个函数。

  • 执行顺序控制: 回调函数通常在主函数完成其任务后执行。

基础示例

// 定义一个接收回调的函数
function greet(name, callback) {
    console.log("你好," + name);
    // 在主逻辑完成后执行回调
    callback();
}

// 定义一个将被作为回调的函数
function sayBye() {
    console.log("再见!");
}

// 调用 greet 函数,并将 sayBye 作为参数传递(注意:不要加括号)
greet("阿杰", sayBye);

执行流程

  1. 调用 greet("阿杰", sayBye)

  2. greet 函数首先打印 "你好,阿杰"。

  3. 接着,greet 内部调用传入的 callback(),即执行 sayBye()

  4. 最后打印 "再见!"。

注意: 传递回调函数时,只需传递函数名(如 sayBye),不要加括号 ()。加括号会立即执行该函数,而不是将其作为参数传递。


回调原理

JavaScript 通常是单线程同步执行的,代码按行依次运行。然而,在实际开发中,我们经常需要延迟执行某些代码,或者等待耗时任务(如网络请求、文件读取)完成后再继续。回调函数正是实现这一机制的关键。

console.log("开始");

setTimeout(function () {
    console.log("setTimeout 内部执行");
}, 2000);

console.log("结束");

输出顺序

  1. 开始

  2. 结束

  3. (等待 2 秒后) setTimeout 内部执行

原理解析

  • setTimeout 是一个异步函数,它接收一个回调函数和一个延迟时间(毫秒)。

  • JavaScript 引擎将回调函数放入任务队列,主线程继续执行后续代码(打印 "结束"),不会阻塞等待。

  • 2 秒后,当主线程空闲时,回调函数才会被执行。


主要场景

处理异步操作

这是回调最广泛的用途,用于处理那些无法立即得到结果的操作:

  • API 请求: 获取服务器数据后处理响应。

  • 文件读写 (Node.js): 文件读取完成后处理内容。

  • 数据库查询: 数据检索成功后进行处理。

  • 定时器: setTimeoutsetInterval

高阶函数中的行为定制

当同一个函数需要根据不同的输入执行不同的逻辑时,回调提供了极大的灵活性。

// 通用计算函数,接收具体的运算逻辑作为回调
function calc(a, b, callback) {
    return callback(a, b);
}

// 定义具体的运算函数
function add(x, y) {
    return x + y;
}

function mul(x, y) {
    return x * y;
}

// 传入不同的回调实现不同的功能
console.log(calc(5, 3, add)); // 输出: 8
console.log(calc(5, 3, mul)); // 输出: 15

事件监听器

JavaScript 是事件驱动的,回调函数是处理用户交互的核心。

document.getElementById("myButton").addEventListener("click", function () {
    console.log("按钮被点击了!");
});

在此例中,匿名函数作为回调,仅在用户点击按钮时触发。

API 数据获取与处理

在处理网络请求时,通常需要将获取到的数据传递给另一个函数进行处理。

// 模拟数据获取函数
function fetchData(callback) {
    // 使用 fetch API 获取数据
    fetch("https://jsonplaceholder.typicode.com/todos/1")
        .then(response => response.json())
        .then(data => callback(data)) // 获取成功后调用回调
        .catch(error => console.error("错误:", error));
}

// 定义处理数据的函数
function handleData(data) {
    console.log("获取到的数据:", data);
}

// 启动获取过程
fetchData(handleData);

局限性与挑战

尽管回调函数功能强大,但在复杂场景下也暴露出了一些问题。

回调地狱

Callback Hell

当多个异步操作需要按顺序执行时,开发者往往需要将回调函数层层嵌套。随着层级加深,代码变得难以阅读和维护,形状像金字塔一样,被称为“回调地狱”。

function step1(callback) {
    setTimeout(() => {
        console.log("步骤 1 完成");
        callback();
    }, 1000);
}

function step2(callback) {
    setTimeout(() => {
        console.log("步骤 2 完成");
        callback();
    }, 1000);
}

function step3(callback) {
    setTimeout(() => {
        console.log("步骤 3 完成");
        callback();
    }, 1000);
}

// 嵌套调用导致代码向右无限延伸
step1(() => {
    step2(() => {
        step3(() => {
            console.log("所有步骤完成");
        });
    });
});

问题点

  • 代码缩进层级过深,逻辑不直观。

  • 错误处理困难,每个层级都需要单独处理异常。

  • 变量作用域管理复杂。

错误处理复杂

在嵌套回调中,如果某一步发生错误,很难统一捕获和处理。通常需要在每个回调中显式检查错误参数(如 Node.js 风格的 error-first 回调)。

function divide(a, b, callback) {
    if (b === 0) {
        // 传递错误对象作为第一个参数
        callback(new Error("不能除以零"), null);
    } else {
        callback(null, a / b);
    }
}

function resultHandler(error, result) {
    if (error) {
        console.log("发生错误:", error.message);
    } else {
        console.log("结果:", result);
    }
}

divide(10, 2, resultHandler);
divide(10, 0, resultHandler);

这种模式要求开发者在每个回调中都编写重复的错误检查逻辑,增加了代码冗余。


现代替代方案

为了解决回调地狱和错误处理难题,ES6 引入了 Promises,ES7 引入了 Async/Await

Promises

承诺

Promise 对象代表一个异步操作的最终完成(或失败)及其结果值。它通过链式调用 (.then()) 避免了深层嵌套。

function step1() {
    return new Promise(resolve => {
        setTimeout(() => {
            console.log("步骤 1 完成");
            resolve();
        }, 1000);
    });
}

function step2() {
    return new Promise(resolve => {
        setTimeout(() => {
            console.log("步骤 2 完成");
            resolve();
        }, 1000);
    });
}

function step3() {
    return new Promise(resolve => {
        setTimeout(() => {
            console.log("步骤 3 完成");
            resolve();
        }, 1000);
    });
}

// 链式调用,代码呈线性结构
step1()
    .then(step2)
    .then(step3)
    .then(() => console.log("所有步骤完成"))
    .catch(error => console.error("发生错误:", error));

优势

  • 扁平化的代码结构,易于阅读。

  • 统一的错误处理机制(通过 .catch())。

Async/Await

异步 / 等待

async/await 是基于 Promise 的语法糖,让异步代码看起来像同步代码,进一步提升了可读性。

async function processSteps() {
    try {
        await step1();
        await step2();
        await step3();
        console.log("所有步骤完成");
    } catch (error) {
        console.error("发生错误:", error);
    }
}

processSteps();

优势

  • 代码逻辑极其清晰,完全消除了嵌套。

  • 可以使用标准的 try...catch 块进行错误处理。

  • 调试更加容易,可以像在同步代码中一样设置断点。