关于JavaScript中回调函数的一切

87 阅读4分钟

关于JavaScript中回调函数的一切

回调函数是每个JavaScript开发者都应该知道的概念之一。回调在数组、定时器函数、承诺、事件处理程序等方面都有应用。

在这篇文章中,我将解释回调函数的概念。同时,我将帮助你区分2种类型的回调:同步和异步。

1.回调函数

你如何编写一条信息来问候一个人呢?

让我们创建一个函数greet(name) ,接受一个name 参数。该函数应该返回问候信息:

function greet(name) {
  return `Hello, ${name}!`;
}
greet('Cristina'); // => 'Hello, Cristina!'

那么向一个人的列表打招呼呢?这可以用一个特殊的数组方法array.map():

const persons = ['Cristina', 'Ana'];
const messages = persons.map(greet);
messages; // => ['Hello, Cristina!', 'Hello, Ana!'] 

persons.map(greet) 接受 数组中的每一项,并使用每一项作为调用参数来调用函数 。, 。persons greet() greet('Cristina') greet('Ana')

有趣的是,persons.map(greet) 方法接受greet() 函数作为一个参数。这样做使greet() 成为一个回调函数

persons.map(greet) 是一个接受另一个函数作为参数的函数,所以它被命名为高阶函数

回调函数作为参数提供给高阶函数高阶函数调用("回调")回调函数来执行一个操作。

重要的是,高阶函数承担了调用回调函数的全部责任,并为其提供了正确的参数。

在前面的例子中,高阶函数persons.map(greet) 负责调用greet() 回调函数,并将数组的每一项作为参数:'Cristina''Ana'

这就带来了一个识别回调的简单规则。如果你已经定义了一个函数,而且你不是自己调用它--而是作为一个参数提供给另一个函数--那么你就创建了一个回调。

你总是可以自己编写使用回调的高阶函数。例如,这里有一个等同于array.map() 方法的版本。

function map(array, callback) {
  const mappedArray = [];
  for (const item of array) { 
    mappedArray.push(
      callback(item)
    );
  }
  return mappedArray;
}
function greet(name) {
  return `Hello, ${name}!`;
}
const persons = ['Cristina', 'Ana'];
const messages = map(persons, greet);
messages; // => ['Hello, Cristina!', 'Hello, Ana!'] 

map(array, callback) 是一个高阶函数,因为它接受一个回调函数作为参数,然后在其主体中调用该回调函数: 。callback(item)

请注意,普通函数(使用function 关键字定义)或箭头函数(使用胖箭头=> 定义)同样可以作为回调。

2.同步回调

按调用方式分,有2种类型的回调:同步回调和异步回调。

同步回调在使用该回调的高阶函数的执行过程中执行。

换句话说,同步回调是阻塞的:在回调执行完毕之前,高阶函数不会完成其执行。

例如,回顾map()greet() 函数:

function map(array, callback) {
  console.log('map() starts');
  const mappedArray = [];
  for (const item of array) { mappedArray.push(callback(item)) }
  console.log('map() completed');
  return mappedArray;
}
function greet(name) {
  console.log('greet() called');
  return `Hello, ${name}!`;
}
const persons = ['Cristina'];
map(persons, greet);
// logs 'map() starts'
// logs 'greet() called'
// logs 'map() completed'

greet() 是一个同步回调,因为它与高阶函数 同时被执行。你可以试试这个map()演示

调用回调的同步方式:

  1. 高阶函数开始执行。'map() starts'
  • 回调函数执行。'greet() called'
  • 最后,高阶函数完成其执行。'map() completed'

2.1 同步回调的例子

很多本地JavaScript类型的方法都使用同步回调。

最常用的是数组方法,如array.map(callback),array.forEach(callback),array.find(callback),array.filter(callback),array.reduce(callback, init)

// Examples of synchronous callbacks on arrays
const persons = ['Ana', 'Elena'];
persons.forEach(
  function callback(name) {
    console.log(name);
  }
);
// logs 'Ana'
// logs 'Elena'
const nameStartingA = persons.find(
  function callback(name) {
    return name[0].toLowerCase() === 'a';
  }
);
nameStartingA; // => 'Ana'
const countStartingA = persons.reduce(
  function callback(count, name) {
    const startsA = name[0].toLowerCase() === 'a';
    return startsA ? count + 1 : count;
  }, 
  0
);
countStartingA; // => 1

string.replace(callback) 字符串类型的方法也接受一个同步执行的回调:

// Examples of synchronous callbacks on strings
const person = 'Cristina';
// Replace 'i' with '1'
person.replace(/./g, 
  function(char) {
    return char.toLowerCase() === 'i' ? '1' : char;
  }
); // => 'Cr1st1na'

3.异步回调

异步回调高阶函数执行执行的。

简单地说,异步回调是无阻塞的:高阶函数在不等待回调的情况下完成其执行。高阶函数确保在某个事件后执行回调。

在下面的例子中,later() 函数的执行有2秒的延迟:

console.log('setTimeout() starts');
setTimeout(function later() {
  console.log('later() called');
}, 2000);
console.log('setTimeout() completed');
// logs 'setTimeout() starts'
// logs 'setTimeout() completed'
// logs 'later() called' (after 2 seconds)

later() 是一个异步回调,因为 开始并完成了它的执行,但 是在通过2秒后执行。试试这个setTimeout(later, 2000) later() 演示

调用回调的异步方式:

  1. 高阶函数开始执行:'setTimeout() starts'
  2. 高阶函数完成其执行:'setTimeout() completed'
  3. 回调函数在2秒后执行:'later() called'

3.1 异步回调的例子

定时器函数异步地调用回调:

setTimeout(function later() {
  console.log('2 seconds have passed!');
}, 2000);
// After 2 seconds logs '2 seconds have passed!' 
setInterval(function repeat() {
  console.log('Every 2 seconds');
}, 2000);
// Each 2 seconds logs 'Every 2 seconds!' 

DOM事件监听器也异步调用事件处理函数(回调函数的一个子类型):

const myButton = document.getElementById('myButton');
myButton.addEventListener('click', function handler() {
  console.log('Button clicked!');
});
// Logs 'Button clicked!' when the button is clicked

4.异步回调函数vs异步函数

放在函数定义前的特殊关键字async ,可以创建一个异步函数:

async function fetchUserNames() {
  const resp = await fetch('https://api.github.com/users?per_page=5');
  const users = await resp.json();
  const names = users.map(({ login }) => login);
  console.log(names);
}

fetchUserNames() 是异步的,因为它的前缀是 。该函数从GitHub中获取 前5个用户。然后从响应对象中提取JSON数据: 。async await fetch('https://api.github.com/users?per_page=5') await resp.json()

异步函数是在承诺之上的语法糖。当遇到表达式await <promise> (注意,调用fetch() 会返回一个承诺),异步函数会暂停执行,直到承诺被解决。

异步回调函数和异步函数是不同的术语。

异步回调函数是由高阶函数以非阻塞方式执行的。但是异步函数在等待承诺(await <promise>)解决时暂停其执行。

然而......你可以使用一个异步函数作为异步回调函数!你可以使用一个异步回调函数。

让我们把异步函数fetchUserNames() 一个在点击按钮时调用的异步回调:

const button = document.getElementById('fetchUsersButton');
button.addEventListener('click', fetchUserNames);

打开Demo,点击Fetch Users。当请求完成后,你会看到一个登录到控制台的用户列表。

5.总结

回调是一个被接受为参数并被另一个函数(高阶函数)执行的函数。

有2种回调函数:同步的和异步的。

同步回调与使用回调的高阶函数同时执行。同步回调是阻塞的

在另一边,异步回调在比高阶函数更晚的时间执行。异步回调是非阻塞的

测验:setTimeout(callback, 0) 是同步还是异步执行callback