在本教程中,您将了解 JavaScript 回调函数,包括同步和异步回调,异步回调的错误处理以及理解什么是回调黑洞。
你也将会知道为什么 JavaScript 存在 promises 或 async/await 函数。
什么是回调
在 JavaScript ,函数是一等公民。因此,您可以将一个函数作为参数传递给另一个函数。
根据定义,回调是一个函数,您将其作为参数传递给另一个函数以供稍后执行。
下面定义一个函数 filter()
,它接受一个数字数组并返回一个奇数数组:
function filter(numbers) {
let results = [];
for (const number of numbers) {
if (number % 2 != 0) {
results.push(number);
}
}
return results;
}
let numbers = [1, 2, 4, 7, 3, 5, 6];
console.log(filter(numbers));
代码如何运行的。
- 首先,定义
filter()
函数接受数字数组并返回新的奇数数组。 - 其次,定义一个
numbers
数组既有奇数又有偶数。 - 最后,调用
filter()
函数从 numbers 数组中取出奇数并打印结果。
如果要返回包含偶数的数组,则需要修改函数 filter()
。为了使 filter()
函数更通用和可重用,您可以:
- 首先,提取
if
块中的逻辑并将其封装在一个单独的函数。 - 其次,将该函数作为参数传递给 filter() 函数。
这是更新后的代码:
function isOdd(number) {
return number % 2 != 0;
}
function filter(numbers, fn) {
let results = [];
for (const number of numbers) {
if (fn(number)) {
results.push(number);
}
}
return results;
}
let numbers = [1, 2, 4, 7, 3, 5, 6];
console.log(filter(numbers, isOdd));
结果是一样的。但是,你可以传递任何函数,只要该函数接受一个参数并返回布尔值给 filter()
函数的第二个参数。
例如,您可以使用 filter()
函数返回偶数数组,如下所示:
function isOdd(number) {
return number % 2 != 0;
}
function isEven(number) {
return number % 2 == 0;
}
function filter(numbers, fn) {
let results = [];
for (const number of numbers) {
if (fn(number)) {
results.push(number);
}
}
return results;
}
let numbers = [1, 2, 4, 7, 3, 5, 6];
console.log(filter(numbers, isOdd));
console.log(filter(numbers, isEven));
根据定义,isOdd
和 isEven
是回调函数或者称为回调。因为 filter()
函数接受一个函数作为参数,所以它被称为高阶函数。
回调可以是匿名函数,它是没有名称的函数,如下所示:
function filter(numbers, callback) {
let results = [];
for (const number of numbers) {
if (callback(number)) {
results.push(number);
}
}
return results;
}
let numbers = [1, 2, 4, 7, 3, 5, 6];
let oddNumbers = filter(numbers, function (number) {
return number % 2 != 0;
});
console.log(oddNumbers);
在此示例中,我们将匿名函数传递给 filter()
函数,而不是使用一个独立的函数。
在 ES6 ,你可以像这样使用箭头函数:
function filter(numbers, callback) {
let results = [];
for (const number of numbers) {
if (callback(number)) {
results.push(number);
}
}
return results;
}
let numbers = [1, 2, 4, 7, 3, 5, 6];
let oddNumbers = filter(numbers, (number) => number % 2 != 0);
console.log(oddNumbers);
回调有两种类型:同步回调和异步回调。
同步回调
同步回调在高阶函数执行时运行。在上面的例子中 isOdd
和 isEven
是同步回调,因为它门在 filter()
函数运行期间也同步运行。
异步回调
异步回调是在高阶函数执行之后运行的回调函数,异步意味着如果 JavaScript 不会等待操作的完成,它将在等待期间运行其余代码。
请注意,JavaScript 是一种单线程编程语言。它通过回调队列和事件循环进行异步操作。
假设你需要开发一个程序,从远程服务器下载一张图片,并在下载完成后进行处理:
function download(url) {
// ...
}
function process(picture) {
// ...
}
download(url);
process(picture);
但是,从远程服务器下载图片需要时间,具体取决于网络速度和图片大小。我们定义下面的 download()
函数使用 setTimeout()
函数来模拟网络请求:
function download(url) {
setTimeout(() => {
// 下载图片的代码
console.log(`Downloading ${url} ...`);
},1000);
}
而下面这段代码模拟 process()
函数的实现:
function process(picture) {
console.log(`Processing ${picture}`);
}
当你执行下面的代码时:
let url = 'https://www.myfreax.com/pic.jpg';
download(url);
process(url);
您将获得以下输出:
Processing https://www.myfreax.com/pic.jpg
Downloading https://www.myfreax.com/pic.jpg ...
这不是您所期望的,因为 process()
函数会 download()
函数之前运行。正确的顺序应该是:
- 下载图片并等待下载完成。
- 处理图片。
要解决此问题,您可以将 process()
函数传递给 download()
函数并在下载完成后在 download()
函数内部执行 process()
函数,如下所示:
function download(url, callback) {
setTimeout(() => {
// 下载图片的代码
console.log(`Downloading ${url} ...`);
// 下载完成后处理图片
callback(url);
}, 1000);
}
function process(picture) {
console.log(`Processing ${picture}`);
}
let url = 'https://www.myfreax.com/pic.jpg';
download(url, process);
输出:
Downloading https://www.myfreax.com/pic.jpg ...
Processing https://www.myfreax.com/pic.jpg
现在,它按预期工作。在此示例中,process()
是传递给异步函数的回调。
当您在异步操作之后使用回调继续执行代码时,回调称为异步回调。为了使代码更简洁,可以将 process()
函数定义为匿名函数:
function download(url, callback) {
setTimeout(() => {
// 下载图片的代码
console.log(`Downloading ${url} ...`);
// 下载完成后处理图片
callback(url);
}, 1000);
}
let url = 'https://www.example.com/pic.jpg';
download(url, function(picture) {
console.log(`Processing ${picture}`);
});
处理错误
download()
函数假设一切正常并且不考虑任何异常。下面的代码引入两个回调:success
和 failure
来分别处理成功和失败的情况:
function download(url, success, failure) {
setTimeout(() => {
console.log(`Downloading the picture from ${url} ...`);
!url ? failure(url) : success(url);
}, 1000);
}
download(
'',
(url) => console.log(`Processing the picture ${url}`),
(url) => console.log(`The '${url}' is not valid`)
);
嵌套回调和回调黑洞
如何下载三张图片并依次处理?一种典型的方法是在回调函数中调用函数,如下所示:
function download(url, callback) {
setTimeout(() => {
console.log(`Downloading ${url} ...`);
callback(url);
}, 1000);
}
const url1 = 'https://www.myfreax.com/pic1.jpg';
const url2 = 'https://www.myfreax.com/pic2.jpg';
const url3 = 'https://www.myfreax.com/pic3.jpg';
download(url1, function (url) {
console.log(`Processing ${url}`);
download(url2, function (url) {
console.log(`Processing ${url}`);
download(url3, function (url) {
console.log(`Processing ${url}`);
});
});
});
输出:
Downloading https://www.myfreax.com/pic1.jpg ...
Processing https://www.myfreax.com/pic1.jpg
Downloading https://www.myfreax.com/pic2.jpg ...
Processing https://www.myfreax.com/pic2.jpg
Downloading https://www.myfreax.com/pic3.jpg ...
Processing https://www.myfreax.com/pic3.jpg
此代码工作得很好。然而,当复杂性显着增加时,这种回调策略无法很好地扩展。
在回调中嵌套许多异步函数被称为回调黑洞:
asyncFunction(function(){
asyncFunction(function(){
asyncFunction(function(){
asyncFunction(function(){
asyncFunction(function(){
....
});
});
});
});
});
为避免回调黑洞,您可以使用 promises 或 async/await 函数。
结论
- 回调是作为稍后执行的参数传递给另一个函数的函数。
- 高阶函数是接受另一个函数作为参数的函数。
- 回调函数可以是同步的或异步的。