⚡ 编程实验室🏗️ HTML🎨 CSS⚡ JavaScript🐍 Python🗄️ SQL☕ Java⚛️ React💚 Vue🟢 Node.js⚙️ C语言🐘 PHP🐹 Go🔷 TypeScript🐬 MySQL🔧 C++🎯 C#🦀 Rust🅱️ Bootstrap💡 jQuery🎸 Django🍃 MongoDB👗 Sass🎪 Kotlin📊 R语言📋 XML📊 Excel🐘 PostgreSQL🐳 Docker🅰️ Angular🎮 游戏🏠 网站首页

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); } } // 看起来像同步代码,实际是异步执行!

  1. 自定义 forEach:编写函数 myForEach(arr, callback),用 for 循环手动实现 Array.prototype.forEach 的功能。确保回调函数的三个参数(当前元素、索引、原数组)都正确传递。然后用 [10, 20, 30, 40] 测试。
  2. 延时消息队列:编写 executeInOrder(tasks, delay) 函数,tasks 是一个回调函数数组,依次执行每个任务,每个任务之间间隔 delay 毫秒。用 setTimeout 的嵌套实现,全部完成后打印"队列执行完毕"。
  3. 简易事件系统(发布-订阅模式):实现一个 miniEventEmitter 对象,包含 on(eventName, callback) 用于注册事件监听器,和 emit(eventName, data) 用于触发事件。同一个事件可以注册多个回调,emit 时所有回调都会被调用。
  4. 函数式数据处理管道:给定数组 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],使用 filter + map + reduce 的组合实现以下操作:保留偶数 → 每个数乘以自己(平方)→ 求和。每一步都使用箭头函数作为回调,最后一行输出结果。
  5. Promise 风格的异步操作:将上面练习中的 fetchUser 改写为返回 Promise 的版本(fetchUserPromise(id)),然后在 async 函数中使用 await 调用它。对比回调和 Promise 两种写法的可读性差异,写一段注释总结你的感受。
Ctrl+Enter
🚀 升级VIP
解锁全部课程+AI助手

🏆 学习排行

加载中...

📊 统计

📖 231 篇
0 完成
🔥 0