myfreax

JavaScript 回调函数

JavaScript 回调函数,包括同步和异步回调,异步回调的错误处理以及理解什么是回调黑洞,你也将会知道为什么 JavaScript 存在 promises 或 async/await 函数

JavaScript 回调函数
JavaScript 回调函数

在本教程中,您将了解 JavaScript 回调函数,包括同步和异步回调,异步回调的错误处理以及理解什么是回调黑洞。

你也将会知道为什么 JavaScript 存在 promisesasync/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));

根据定义,isOddisEven​​ 是回调函数或者称为回调。因为 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);

回调有两种类型:同步回调和异步回调。

同步回调

同步回调在高阶函数执行时运行。在上面的例子中 isOddisEven  是同步回调,因为它门在 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() 函数假设一切正常并且不考虑任何异常。下面的代码引入两个回调:successfailure 来分别处理成功和失败的情况:

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(){
                    ....
                });
            });
        });
    });
});

为避免回调黑洞,您可以使用 promisesasync/await 函数。

结论

  • 回调是作为稍后执行的参数传递给另一个函数的函数。
  • 高阶函数是接受另一个函数作为参数的函数。
  • 回调函数可以是同步的或异步的。

内容导航