本文深入解析JavaScript回调函数的核心概念,阐述其作为高阶函数参数的运行机制。重点讨论单线程环境下回调在API调用等异步场景中的应用,分析“回调地狱”的代码结构缺陷,并介绍使用Promise、async-await及async.js库(如waterfall和series)来优化异步流程的最佳实践。
引言
JavaScript 回调函数是每位成功的 JS 开发者必须掌握的核心概念。阅读此文后,你将彻底解决以往使用回调时的所有难题。
在深入之前,务必夯实对函数的基础认知。
快速回顾:JavaScript 函数机制
函数的定义与本质
函数是封装代码逻辑的构件,用于执行特定任务。它使代码更易于调试和维护,并支持代码复用。
只需定义一次函数,随需调用,避免重复编写相同代码。
函数的声明方式
以下介绍 JavaScript 中声明函数的几种方法。
- 构造函数法:通过“Function”构造函数创建。从技术角度看,此法效率低于函数表达式或声明语句语法。
- 函数表达式法:通常与变量赋值类似。函数体被视为表达式并赋值给变量。定义出的函数可为命名或匿名函数。 无名称的函数称为匿名函数,它是自调用的,即自动执行自身。这种行为称为立即调用函数表达式(IIFE)。
- 函数声明法:这是 JavaScript 中传统的常用方法。在“function”关键字后指定函数名,随后可选地列出参数。
函数体内必须通过 return 语句向调用方返回值。遇到 return 后函数停止执行。函数内的参数充当局部变量。
在函数内部声明的变量属于该函数的局部变量,仅限函数内部访问。因此,不同函数可使用同名变量而不冲突。
函数的调用时机
在以下任一情况下,之前声明的函数将被调用:
- 事件触发时,如用户点击按钮或从下拉列表选择选项等。
- 在 JavaScript 代码中显式调用该函数时。
- 函数自动调用,前文已讨论匿名函数表达式的情况。
通过 () 运算符调用该函数。
回调函数的定义
据 MDN 描述:回调函数是作为参数传递给另一个函数的函数,随后在外部函数内部调用该回调函数以完成特定操作。
通俗解释:回调函数是在另一个函数执行完毕后立即执行的函数。它是作为参数传递给另一个 JavaScript 函数的函数,并在传给的函数内部执行。
在 JavaScript 中,函数被视为一类对象。这意味着数字、函数或变量可与语言中的其他实体等同。作为一类对象,函数可作为变量传递给其他函数,也可从其他函数返回。
能执行此类操作的函数称为高阶函数。回调函数实际上是一种模式。“模式”指解决软件开发中常见问题的有效方法。建议将回调函数作为回调模式使用。
为何需要回调机制
客户端 JavaScript 在浏览器中运行,主进程为单线程事件循环。若在单线程事件循环中执行长时间操作,将导致阻塞。这在技术上是不利的,因为进程在等待操作完成时会停止处理其他事件。
例如,alert 语句被视为浏览器中 JavaScript 的阻塞代码之一。运行 alert 时,在关闭对话框前无法进行任何浏览器交互。为防止阻塞长时间操作,我们使用回调。
让我们深入探讨,以便准确了解何时使用回调。

获取并显示消息的函数示例
在上述代码片段中,首先执行 getMessage() 函数,随后执行 displayMessage()。两者均在浏览器控制台窗口显示消息,且均立即执行。
在某些情况下,部分代码不会立即执行。例如,若假设 getMessage() 函数执行 API 调用,则需向服务器发送请求并等待响应。此时应如何处理?
回调函数的使用方法
我认为在前例中实现回调函数比单纯讲解语法更有益。修改后的代码段显示在下方截图中。

使用回调函数显示消息
为使用回调函数,需执行无法立即显示结果的任务。为模拟此行为,我们使用 JavaScript 的 setTimeout() 函数。该函数暂停两秒后,在控制台窗口显示消息“Hi,there”。
“显示的消息”将显示在浏览器控制台窗口。在此情况下,首先需等待 getMessage() 函数。成功执行此函数后,再执行 displayMessage() 函数。
回调的工作原理
让我解释前例在幕后发生的过程。
从前例可见,在 getMessage() 函数中传递了两个参数。第一个参数是 msg 变量,显示在浏览器控制台窗口;第二个参数是回调函数。
现在,你可能想知道为何将回调函数作为参数传递——要实现回调函数,必须将一个函数作为参数传给另一个函数。
在 getMessage() 完成任务后,我们将调用回调函数。此后,当调用 getMessage() 函数时,将引用传给 displayMessage() 函数,该函数即为回调函数。
注意,当调用 getMessage() 函数时,我们仅将其引用传给 displayMessage() 函数。这就是为什么你未在其旁边看到函数调用运算符,即 () 符号。
JavaScript 回调是异步的吗?
JavaScript 被视为单线程脚本语言。单线程指 JavaScript 一次执行一个代码块。当 JavaScript 忙于执行一个块时,它无法移至下一个块。
换言之,我们可以认为 JavaScript 代码本质上总是阻塞的。但这种阻塞性使我们在某些情况下无法编写代码,因为在这些情况下我们无法在执行特定任务后立即得到结果。
我谈论的任务包括以下情况:
- 通过对某些端点进行 API 调用来获取数据。
- 通过发送网络请求从远程服务器获取资源(如文本文件、图像文件、二进制文件等)。
为处理这些情况,必须编写异步代码,而回调函数是处理这些情况的一种方法。因此,从本质上说,回调函数是异步的。
JavaScript 回调地狱问题
当多个异步函数接连执行时,会产生回调地狱。它也被称为厄运金字塔。
假设你要获取所有 Github 用户的列表。然后在用户中搜索 JavaScript 库的主要贡献者。再然后,你想要在用户中获取姓名为 John 的人员的详细信息。
为了在回调的帮助下实现这个功能,代码应该如下所示:
http.get('https://api.github.com/users', function(users) {
/* Display all users */
console.log(users);
http.get('https://api.github.com/repos/javascript/contributors?q=contributions&order=desc', function(contributors) {
/* Display all top contributors */
console.log(contributors);
http.get('https://api.github.com/users/Jhon', function(userData) {
/* Display user with username 'Jhon' */
console.log(userData);
});
});
});
从上面的代码片段中,你可以看到代码变得更加难以理解,以及难以维护和修改。这是由回调函数的嵌套而引发的。
如何避免回调地狱?
可以使用多种技术来避免回调地狱,如下所示。
- 使用 Promise
- 借助 async-await
- 使用 async.js 库
使用 Async.js 库
让我们谈谈怎样用 async.js 库避免回调地狱。
根据 async.js 官方网站的描述:Async 是一个工具模块,它提供了直接、强大的函数来使用异步 JavaScript。
Async.js 总共提供约 70 个函数。现在,我们将仅讨论其中两个,即 async.waterfall() 和 async.series()。
async.waterfall()
当你要一个接一个地运行某些任务,然后将结果从上一个任务传到下一个任务时,这个函数非常有用。它需要一个函数“任务”数组和一个最终的“回调”函数,它会在“任务”数组中所有的函数完成后,或者用错误对象调用“回调”之后被调用。
var async = require('async');
async.waterfall([
function(callback) {
/*
Here, the first argument value is null, it indicates that
the next function will be executed from the array of functions.
If the value was true or any string then final callback function
will be executed, other remaining functions in the array
will not be executed.
*/
callback(null, 'one', 'two');
},
function(param1, param2, callback) {
// param1 now equals 'one' and param2 now equals 'two'
callback(null, 'three');
},
function(param1, callback) {
// param1 now equals 'three'
callback(null, 'done');
}
], function (err, result) {
/*
This is the final callback function.
result now equals 'done'
*/
});
async.series()
当你要运行一个函数然后在所有函数成功执行后需要获取结果时,它很有用。 async.waterfall() 和 async.series() 之间的主要区别在于, async.series() 不会将数据从一个函数传递到另一个函数。
async.series([
function(callback) {
// do some stuff ...
callback(null, 'one');
},
function(callback) {
// do some more stuff ...
callback(null, 'two');
}
],
// optional callback
function(err, results) {
// results is now equal to ['one', 'two']
});
Javascript 回调与闭包的关系
闭包概念
用技术术语来说,闭包是捆绑在一起的函数的组合,引用了其周围的状态。
简而言之,闭包允许从内部函数访问外部函数的作用域。
要使用闭包,我们需要在一个函数内部定义另一个函数。然后,我们需要将其返回或传给另一个函数。
回调概念
从概念上讲,回调类似于闭包。回调基本上是把一个函数作为另一个函数的用法。
结语
希望本文能消除你对 javascript 回调函数的所有疑问。如果你觉得这篇文章有帮助,请与他人分享。
到此这篇关于关于JavaScript回调函数的文章就介绍到这了,更多相关JavaScript回调函数内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!