JS 回调函数
理解回调函数和异步 · 难度:进阶 · +15XP
JavaScript 回调函数详解
回调函数(Callback Function)是理解 JavaScript 异步编程的基石。在 JavaScript 中,函数是一等公民——它可以被赋值给变量、作为参数传递给另一个函数、也可以作为另一个函数的返回值。当一个函数被作为参数传递给另一个函数,并且在未来的某个时刻由后者"回头调用",这个被传递的函数就叫做回调函数。理解回调是通往 Promise、async/await 等现代异步模式的第一道门。
回调函数的基本概念
回调函数本质上就是"把一个函数当作参数传给另一个函数,让后者在适当的时候执行它"。这不是 JavaScript 独有的概念,但由于 JavaScript 的事件驱动和异步特性,回调在 JS 中使用得格外频繁。
// 最简单的回调示例
function greet(name) {
console.log('你好,' + name + '!');
}
function processUserInput(callback) {
let userName = '小明';
callback(userName); // 在这里"回头调用"传入的函数
}
// 将 greet 函数作为回调传入
processUserInput(greet); // 输出: 你好,小明!
// 更常见的写法:直接传入匿名函数
processUserInput(function(name) {
console.log('欢迎回来,尊贵的 ' + name);
});
// 箭头函数写法(最简洁)
processUserInput(name => {
console.log(哈喽,${name},今天过得好吗?);
});
同步回调 vs 异步回调 —— 关键区别
理解这两者的区别至关重要。同步回调在调用它的函数体内立即执行,会阻塞后续代码;异步回调被注册后不会立即执行,而是在未来某个时刻(如定时器到期、网络请求完成、事件触发)才被调用,不会阻塞后续代码。JavaScript 通过事件循环机制实现了异步回调。
| 类型 | 执行时机 | 是否阻塞 | 常见示例 |
|---|---|---|---|
| 同步回调 | 立即执行 | 阻塞后续代码 | 数组方法:forEach, map, filter, sort |
| 异步回调 | 未来某个时刻执行 | 不阻塞 | setTimeout, addEventListener, AJAX, fs.readFile |
// 同步回调示例 —— 数组方法立即执行
console.log('开始');
let nums = [1, 2, 3, 4, 5];
nums.forEach(function(item, index) {
console.log(索引${index}: 值${item});
});
console.log('forEach 执行完毕');
// 输出顺序: 开始 → 索引0~4 → forEach 执行完毕
// 所有回调都是同步执行的,按顺序输出
// 异步回调示例 —— setTimeout 延迟执行
console.log('开始');
setTimeout(function() {
console.log('2秒后的回调');
}, 2000);
console.log('结束');
// 输出顺序: 开始 → 结束 → 2秒后的回调
// 注意 "结束" 在 "2秒后的回调" 之前输出!
实际应用场景一:事件监听
// 浏览器中的点击事件(异步回调)
document.getElementById('btn').addEventListener('click', function(event) {
console.log('按钮被点击了!');
console.log('点击坐标:', event.clientX, event.clientY);
});
// 回调函数在每次点击事件发生时被调用
// Node.js 中的文件读取(异步回调,错误优先风格)
const fs = require('fs');
fs.readFile('data.txt', 'utf8', function(err, data) {
if (err) {
console.error('读取文件失败:', err.message);
return; // 出错时提前返回
}
console.log('文件内容:', data);
// 处理文件数据...
});
// 注意回调的第一个参数通常是 err(错误优先回调)
实际应用场景二:封装异步 API
// 封装一个模拟的 AJAX 请求函数(回调风格)
function fetchUser(id, callback) {
console.log(正在请求用户 ${id} 的数据...);
// 用 setTimeout 模拟网络延迟
setTimeout(function() {
// 模拟:90% 概率成功,10% 概率失败
if (Math.random() > 0.1) {
// 成功:调用 callback,第一个参数 null 表示没有错误
callback(null, {
id: id,
name: '用户' + id,
email: 'user' + id + '@example.com',
createdAt: new Date().toISOString()
});
} else {
// 失败:第一个参数是错误对象
callback(new Error('网络请求超时,请重试'));
}
}, 1000 + Math.random() * 2000); // 1-3秒随机延迟
}
// 使用封装的 API
fetchUser(42, function(err, user) {
if (err) {
console.error('获取用户失败:', err.message);
return;
}
console.log('用户数据:', user);
console.log('姓名:', user.name);
console.log('邮箱:', user.email);
});
回调地狱(Callback Hell)—— 回调的最大痛点
当多个异步操作需要按顺序依次执行时(后一步依赖前一步的结果),回调会导致代码层层嵌套,形成向右严重缩进的"金字塔"结构。这种代码极难阅读、调试和维护,被称为"回调地狱"。
// 回调地狱示例 —— 请勿在实际项目中这样写!
getUser(userId, function(err, user) {
if (err) return console.error(err);
getPosts(user.id, function(err, posts) {
if (err) return console.error(err);
getComments(posts[0].id, function(err, comments) {
if (err) return console.error(err);
getLikes(comments[0].id, function(err, likes) {
if (err) return console.error(err);
getAuthor(likes[0].userId, function(err, author) {
if (err) return console.error(err);
console.log('最终结果:', author.name, likes.length);
});
});
});
});
});
// 解决方案一:使用 Promise 链式调用
getUser(userId)
.then(user => getPosts(user.id))
.then(posts => getComments(posts[0].id))
.then(comments => getLikes(comments[0].id))
.then(likes => getAuthor(likes[0].userId))
.then(author => console.log('结果:', author.name))
.catch(err => console.error('出错:', err));
// 解决方案二:使用 async/await(最推荐)
async function getFullData(userId) {
try {
let user = await getUser(userId);
let posts = await getPosts(user.id);
let comments = await getComments(posts[0].id);
let likes = await getLikes(comments[0].id);
let author = await getAuthor(likes[0].userId);
return { author: author.name, likeCount: likes.length };
} catch (err) {
console.error('出错:', err);
}
}
// 看起来像同步代码,实际是异步执行!
- 自定义 forEach:编写函数 myForEach(arr, callback),用 for 循环手动实现 Array.prototype.forEach 的功能。确保回调函数的三个参数(当前元素、索引、原数组)都正确传递。然后用 [10, 20, 30, 40] 测试。
- 延时消息队列:编写 executeInOrder(tasks, delay) 函数,tasks 是一个回调函数数组,依次执行每个任务,每个任务之间间隔 delay 毫秒。用 setTimeout 的嵌套实现,全部完成后打印"队列执行完毕"。
- 简易事件系统(发布-订阅模式):实现一个 miniEventEmitter 对象,包含 on(eventName, callback) 用于注册事件监听器,和 emit(eventName, data) 用于触发事件。同一个事件可以注册多个回调,emit 时所有回调都会被调用。
- 函数式数据处理管道:给定数组 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],使用 filter + map + reduce 的组合实现以下操作:保留偶数 → 每个数乘以自己(平方)→ 求和。每一步都使用箭头函数作为回调,最后一行输出结果。
- Promise 风格的异步操作:将上面练习中的 fetchUser 改写为返回 Promise 的版本(fetchUserPromise(id)),然后在 async 函数中使用 await 调用它。对比回调和 Promise 两种写法的可读性差异,写一段注释总结你的感受。