← JavaScript箭头函数 JavaScript高阶函数 →

JavaScript回调函数

原创 2026-03-14 JavaScript 已有人查阅

JavaScript回调函数指南:从入门到精通

一、回调函数是什么

回调函数是JavaScript中最基础也最重要的概念之一。回调函数就是作为参数传递给另一个函数的函数,它会在特定操作完成后被执行。

让我用生活中的例子帮你理解:你去餐厅点餐,服务员给你一个取餐器(这是回调函数),后厨做好菜后,取餐器会响(回调被执行),你就可以去取餐了。这样你不需要站在厨房门口干等,可以做自己的事。

// 最基本的回调函数示例
function orderFood(foodName, callback) {
    console.log('点了一份:' + foodName);
    console.log('后厨正在制作中...');
    
    // 模拟做饭需要时间
    setTimeout(() => {
        console.log(foodName + ' 做好了');
        callback(); // 执行回调,通知顾客取餐
    }, 2000);
}

function takeMeal() {
    console.log('取到餐了,开始享用!');
}

// 把takeMeal作为回调传给orderFood
orderFood('红烧牛肉面', takeMeal);

二、为什么需要回调函数

JavaScript是单线程执行的,这意味着它一次只能做一件事。如果没有回调函数,遇到耗时操作(比如网络请求、文件读取)时,程序就会卡住,直到操作完成。

// 没有回调的问题演示
console.log('开始点餐');

// 模拟同步的耗时操作(假设需要3秒)
function syncMakeFood() {
    let start = Date.now();
    while (Date.now() - start < 3000) {
        // 空循环,阻塞3秒
    }
    return '餐做好了';
}

let food = syncMakeFood();
console.log(food);
console.log('继续执行他代码'); // 这行要等3秒后才能执行

// 使用回调解决阻塞问题
console.log('开始点餐(使用回调)');

function asyncMakeFood(callback) {
    setTimeout(() => {
        callback('餐做好了(异步)');
    }, 3000);
}

asyncMakeFood((result) => {
    console.log(result);
});
console.log('继续执行他代码'); // 立即执行,不用等3秒

三、回调函数的两种类型

1. 同步回调

同步回调会在主函数执行过程中立即被调用,就像排队一样一个一个来。

// 数组遍历是典型的同步回调
const numbers = [1, 2, 3, 4, 5];

// forEach的回调是同步执行的
numbers.forEach((num, index) => {
    console.log(`第${index + 1}个数字是:${num}`);
});
console.log('遍历结束'); // 这行会在所有回调执行完后才执行

// 另一个同步回调的例子
function processUserData(user, validationCallback, formatCallback) {
    // 先验证
    let isValid = validationCallback(user);
    if (!isValid) {
        console.log('用户数据验证失败');
        return;
    }
    
    // 再格式化
    let formattedData = formatCallback(user);
    console.log('处理完成:', formattedData);
}

const user = { name: '张三', age: 25 };
processUserData(
    user,
    (u) => u.age >= 18 && u.age <= 60, // 验证回调
    (u) => `${u.name}(${u.age}岁)`    // 格式化回调
);

2. 异步回调

异步回调会在未来某个时间点被执行,不会阻塞后续代码。

// 事件监听是典型的异步回调
console.log('注册点击事件');

document.getElementById('myButton').addEventListener('click', () => {
    console.log('按钮被点击了'); // 这行代码不知道什么时候执行
});

console.log('事件注册完成'); // 这行立即执行

// setTimeout是另一个常见例子
console.log('设置定时器');

setTimeout(() => {
    console.log('2秒时间到了'); // 2秒后才执行
}, 2000);

console.log('定时器已设置'); // 立即执行

// 项目开发中的异步回调:网络请求
function fetchUserData(userId, successCallback, errorCallback) {
    console.log(`开始获取用户${userId}的数据...`);
    
    // 模拟网络请求
    setTimeout(() => {
        // 随机模拟成功或失败
        let isSuccess = Math.random() > 0.3;
        
        if (isSuccess) {
            const userData = {
                id: userId,
                name: '李四',
                email: 'lisi@example.com',
                createTime: '2026-03-14'
            };
            successCallback(userData);
        } else {
            errorCallback('网络错误,请稍后重试');
        }
    }, 1500);
    
    console.log('请求已发送,继续处理他任务');
}

// 使用fetchUserData
fetchUserData(
    1001,
    (data) => {
        console.log('获取成功:', data);
    },
    (error) => {
        console.error('获取失败:', error);
    }
);

四、回调函数的实际应用场景

1. 数组方法中的回调

const students = [
    { name: '王小明', score: 85, class: '三年二班' },
    { name: '李小丽', score: 92, class: '三年一班' },
    { name: '张小强', score: 78, class: '三年二班' },
    { name: '刘小美', score: 96, class: '三年一班' }
];

// filter回调:筛选条件
const topStudents = students.filter(student => student.score >= 90);
console.log('优秀学生:', topStudents);

// map回调:数据转换
const studentCards = students.map(student => ({
    displayName: `${student.name}(${student.class})`,
    grade: student.score >= 60  '及格' : '不及格'
}));
console.log('学生卡片:', studentCards);

// sort回调:排序规则
const scoreRank = [...students].sort((a, b) => b.score - a.score);
console.log('成绩排名:', scoreRank);

// reduce回调:累计计算
const totalScore = students.reduce((sum, student) => sum + student.score, 0);
const averageScore = totalScore / students.length;
console.log('平均分:', averageScore.toFixed(2));

2. 事件处理中的回调

class SearchBox {
    constructor(inputId, resultId) {
        this.input = document.getElementById(inputId);
        this.result = document.getElementById(resultId);
        this.searchTimeout = null;
        
        // 输入事件回调:实时搜索
        this.input.addEventListener('input', (e) => {
            this.handleInput(e.target.value);
        });
        
        // 焦点事件回调
        this.input.addEventListener('focus', () => {
            this.result.style.display = 'block';
        });
        
        // 失去焦点事件回调(延迟隐藏)
        this.input.addEventListener('blur', () => {
            setTimeout(() => {
                this.result.style.display = 'none';
            }, 200);
        });
    }
    
    handleInput(keyword) {
        // 清除之前的定时器
        clearTimeout(this.searchTimeout);
        
        if (keyword.length < 2) {
            this.result.innerHTML = '<div>请输入至少2个字符</div>';
            return;
        }
        
        // 防抖处理:用户停止输入300ms后才真正搜索
        this.searchTimeout = setTimeout(() => {
            this.performSearch(keyword);
        }, 300);
    }
    
    performSearch(keyword) {
        // 模拟搜索API调用
        console.log(`搜索关键词:${keyword}`);
        
        // 这里可以发ajax请求
        const mockResults = [
            { title: 'JavaScript入门教程', url: '#' },
            { title: 'JavaScript高级编程', url: '#' },
            { title: 'JavaScript设计模式', url: '#' }
        ].filter(item => 
            item.title.includes(keyword)
        );
        
        this.renderResults(mockResults);
    }
    
    renderResults(results) {
        if (results.length === 0) {
            this.result.innerHTML = '<div>没有找到相关结果</div>';
            return;
        }
        
        let html = '';
        results.forEach(item => {
            html += `<div><a href="${item.url}">${item.title}</a></div>`;
        });
        this.result.innerHTML = html;
    }
}

// 使用示例(假设有对应HTML元素)
// const searchBox = new SearchBox('searchInput', 'searchResult');

3. Node.js中的回调

// 模拟Node.js的文件系统操作
const fs = {
    readFile: function(filename, encoding, callback) {
        console.log(`开始读取文件:${filename}`);
        
        setTimeout(() => {
            // 模拟文件内容
            const mockFiles = {
                'config.json': '{"port": 3000, "env": "development"}',
                'data.txt': '这是一些测试数据\n第二行内容',
                'users.csv': 'id,name,age\n1,赵六,28\n2,钱七,32'
            };
            
            const content = mockFiles[filename];
            if (content) {
                callback(null, content); // 第一个参数是错误,成功时为null
            } else {
                callback(new Error(`文件不存在:${filename}`));
            }
        }, 1000);
    },
    
    writeFile: function(filename, data, callback) {
        console.log(`开始写入文件:${filename}`);
        
        setTimeout(() => {
            console.log(`文件写入成功:${filename}`);
            console.log('写入内容:', data);
            callback(null); // 成功回调,没有错误
        }, 800);
    }
};

// 读取配置文件的典型回调模式
console.log('应用启动中...');

fs.readFile('config.json', 'utf8', (err, data) => {
    if (err) {
        console.error('读取配置失败:', err.message);
        return;
    }
    
    try {
        const config = JSON.parse(data);
        console.log('配置加载成功:', config);
        
        // 根据配置执行后续操作
        if (config.env === 'development') {
            console.log('开发环境,启动调试模式');
        }
        
        // 写日志文件
        fs.writeFile('startup.log', `启动时间:${new Date().toLocaleString()}`, (err) => {
            if (err) {
                console.error('写日志失败');
            } else {
                console.log('启动日志已记录');
            }
        });
        
    } catch (parseErr) {
        console.error('配置文件格式错误:', parseErr.message);
    }
});

console.log('应用继续执行他初始化任务...');

五、回调地狱及解决方案

回调虽然好用,但嵌套多了就会变成"回调地狱"(Callback Hell):

// 糟糕的写法:回调地狱
function placeOrder(callback) {
    setTimeout(() => {
        console.log('1. 订单已创建');
        callback(() => {
            setTimeout(() => {
                console.log('2. 支付成功');
                callback(() => {
                    setTimeout(() => {
                        console.log('3. 商家接单');
                        callback(() => {
                            setTimeout(() => {
                                console.log('4. 骑手取餐');
                                callback(() => {
                                    setTimeout(() => {
                                        console.log('5. 订单送达');
                                    }, 1000);
                                });
                            }, 1000);
                        });
                    }, 1000);
                });
            }, 1000);
        });
    }, 1000);
}

// 这种代码难以阅读和维护

解决方案1:命名函数

// 把回调拆分成命名函数
function createOrder(callback) {
    setTimeout(() => {
        console.log('1. 订单已创建');
        callback();
    }, 1000);
}

function processPayment(callback) {
    setTimeout(() => {
        console.log('2. 支付成功');
        callback();
    }, 1000);
}

function acceptOrder(callback) {
    setTimeout(() => {
        console.log('3. 商家接单');
        callback();
    }, 1000);
}

function pickupOrder(callback) {
    setTimeout(() => {
        console.log('4. 骑手取餐');
        callback();
    }, 1000);
}

function deliverOrder() {
    setTimeout(() => {
        console.log('5. 订单送达');
    }, 1000);
}

// 调用时更清晰
createOrder(() => {
    processPayment(() => {
        acceptOrder(() => {
            pickupOrder(() => {
                deliverOrder();
            });
        });
    });
});

解决方案2:使用Promise

// 用Promise改写
function createOrder() {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log('1. 订单已创建');
            resolve();
        }, 1000);
    });
}

function processPayment() {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log('2. 支付成功');
            resolve();
        }, 1000);
    });
}

function acceptOrder() {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log('3. 商家接单');
            resolve();
        }, 1000);
    });
}

function pickupOrder() {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log('4. 骑手取餐');
            resolve();
        }, 1000);
    });
}

function deliverOrder() {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log('5. 订单送达');
            resolve();
        }, 1000);
    });
}

// 链式调用,清晰多了
createOrder()
    .then(processPayment)
    .then(acceptOrder)
    .then(pickupOrder)
    .then(deliverOrder)
    .then(() => {
        console.log('订单流程完成');
    });

解决方案3:使用async/await

// 较优雅的写法
async function orderProcess() {
    await createOrder();
    await processPayment();
    await acceptOrder();
    await pickupOrder();
    await deliverOrder();
    console.log('订单流程完成');
}

orderProcess();

六、回调函数的实践

1. 错误处理优先

// Node.js风格的错误优先回调
function readConfig(path, callback) {
    setTimeout(() => {
        try {
            // 模拟文件读取出错
            if (path === 'config.json') {
                const config = { port: 3000, debug: true };
                callback(null, config); // 第一个参数为null表示没有错误
            } else {
                throw new Error('配置文件不存在');
            }
        } catch (error) {
            callback(error, null); // 有错误时第一个参数传错误对象
        }
    }, 500);
}

// 使用错误优先回调
readConfig('config.json', (err, config) => {
    if (err) {
        console.error('读取失败:', err.message);
        // 处理错误,比如使用默认配置
        config = { port: 8080, debug: false };
        console.log('使用默认配置');
    }
    console.log('配置加载成功:', config);
});

2. 回调只执行一次

function fetchData(callback) {
    let executed = false;
    
    setTimeout(() => {
        if (!executed) {
            executed = true;
            callback(null, { data: '成功获取数据' });
        }
    }, 1000);
    
    // 防止回调被多次调用
    setTimeout(() => {
        if (!executed) {
            executed = true;
            callback(new Error('请求超时'));
        }
    }, 5000);
}

3. 使用具名函数提高可读性

// 不好的做法:大量匿名函数嵌套
userService.getUser(123, (err, user) => {
    if (err) handleError(err);
    orderService.getOrders(user.id, (err, orders) => {
        if (err) handleError(err);
        paymentService.getPayments(user.id, (err, payments) => {
            // 难以阅读
        });
    });
});

// 好的做法:提取具名函数
function handleUserData(err, user) {
    if (err) {
        handleError(err);
        return;
    }
    orderService.getOrders(user.id, handleOrders);
}

function handleOrders(err, orders) {
    if (err) {
        handleError(err);
        return;
    }
    paymentService.getPayments(user.id, handlePayments);
}

function handlePayments(err, payments) {
    if (err) {
        handleError(err);
        return;
    }
    renderUserDashboard(user, orders, payments);
}

userService.getUser(123, handleUserData);

七、个人经验

经过多年的JavaScript开发,我对回调函数的使用有了这些体会:

选择回调的场景

  1. 简单的一次性操作:比如数组遍历、事件监听

  2. 不需要复杂错误处理:简单的回调函数就够了

  3. 兼容旧代码:维护老项目时还得用回调

避免回调的场景

  1. 多层嵌套:超过3层嵌套就应该考虑用Promise

  2. 需要错误处理:回调的错误处理比较麻烦

  3. 并行操作:多个异步操作需要同步时,用Promise.all更好

给代码号学习编程的建议

刚开始接触异步编程时,建议循序渐进:

// 第1步:先理解同步回调
[1, 2, 3].forEach(item => console.log(item));

// 第2步:再接触简单的异步回调
setTimeout(() => console.log('延迟执行'), 1000);

// 第3步:尝试自己写带回调的函数
function calculate(a, b, callback) {
    let result = a + b;
    callback(result);
}

// 第4步:处理多层回调,学会拆解
function step1(cb) { setTimeout(() => { console.log(1); cb(); }, 1000); }
function step2(cb) { setTimeout(() => { console.log(2); cb(); }, 1000); }
function step3(cb) { setTimeout(() => { console.log(3); cb(); }, 1000); }

// 然后逐步引入Promise和async/await

八、面试常考点

如果你正在准备面试,这些回调相关的问题值得关注:

// 1. 回调的执行时机
console.log('1');
setTimeout(() => console.log('2'), 0);
console.log('3');
// 输出顺序:1, 3, 2

// 2. this指向问题
const obj = {
    name: '测试对象',
    method: function() {
        setTimeout(function() {
            console.log(this.name); // undefined(this指向window)
        }, 100);
        
        setTimeout(() => {
            console.log(this.name); // '测试对象'(箭头函数保留this)
        }, 100);
    }
};

// 3. 回调与闭包
function createCallback(x) {
    return function() {
        console.log(x); // 闭包捕获变量x
    };
}
setTimeout(createCallback(10), 1000); // 1秒后输出10

回调函数是JavaScript异步编程的基石,理解它对你掌握整个语言非常重要。虽然现在有了更现在的Promise和async/await,但回调依然广泛存在于各种API和库中。

记住:回调不是过时的技术,而是基础。就像盖房子,你可以用现在工具,但还是要懂地基怎么打。掌握了回调,你才能更好地理解Promise的设计思想,写出更健壮的异步代码。

← JavaScript箭头函数 JavaScript高阶函数 →
分享笔记 (共有 篇笔记)
验证码:
微信公众号