回到基础:什么是JavaScript中的回调 (2)

44 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情

命名功能

在JavaScript中创建命名函数有两种主要方法:函数表达式和函数声明。两者都可以与回调一起使用。

函数声明包括使用Function关键字创建函数并为其命名:

function myCallback() {... }
setTimeout(myCallback, 1000);

函数表达式包括创建一个函数并将其赋值给一个变量:

const myCallback = function() { ... };
setTimeout(myCallback, 1000);

或者:

const myCallback = () => { ... };
setTimeout(myCallback, 1000);

我们还可以用function关键字标记匿名函数:

setTimeout(function myCallback()  { ... }, 1000);

以这种方式命名或标记回调函数的好处是它有助于调试。让我们的函数抛出一个错误:

setTimeout(function myCallback() { throw new Error('Boom!'); }, 1000);

// Uncaught Error: Boom!

使用命名函数,我们可以确切地看到错误发生在哪里。然而,看看当我们删除名称时会发生什么:

setTimeout(function() { throw new Error('Boom!'); }, 1000);

// Uncaught Error: Boom!
// <anonymous>  file:///home/jim/Desktop/index.js:18
// setTimeout handler*  file:///home/jim/Desktop/index.js:18

在这个小而独立的示例中,这不是什么大问题,但是随着代码库的增长,这一点是需要注意的。甚至有一个ESLint规则来强制执行这种行为。

JavaScript回调函数的常用用例

JavaScript回调函数的用例广泛而多样。正如我们所看到的,它们在处理异步代码(如Ajax请求)和响应事件(如表单提交)时非常有用。现在让我们再看几个我们找到回调的地方。

数组的方法

遇到回调的另一个地方是在JavaScript中使用数组方法时。随着编程的发展,您将越来越多地使用这种方法。例如,假设你想对一个数组中的所有数字求和,考虑以下简单的实现:

const arr = [1, 2, 3, 4, 5];
let tot = 0;
for(let i=0; i<arr.length; i++) {
  tot += arr[i];
}
console.log(tot); //15

虽然这样可以工作,但更简洁的实现可能使用Array。Reduce,你猜对了,它使用回调函数对数组中的所有元素执行操作:

const arr = [1, 2, 3, 4, 5];
const tot = arr.reduce((acc, el) => acc + el);
console.log(tot);
// 15

node . js

还应该注意到Node.js及其整个生态系统严重依赖于基于回调的代码。例如,下面是规范Hello, World!例子:

const http = require('http');

http.createServer((request, response) => {
  response.writeHead(200);
  response.end('Hello, World!');
}).listen(3000);

console.log('服务运行于 http://localhost:3000');

无论您是否使用过Node,这段代码现在应该很容易理解。实际上,我们需要Node的http模块并调用它的createServer方法,我们将向该方法传递一个匿名箭头函数。每当Node在端口3000上接收到请求时,都会调用此函数,它将以200状态和文本“Hello, World!”

Node还实现了一种称为错误优先回调的模式。这意味着回调的第一个参数为错误对象保留,而回调的第二个参数为任何成功响应数据保留。

下面是Node文档中的一个例子,展示了如何读取文件:

const fs = require('fs');
fs.readFile('/etc/hosts', 'utf8', function (err, data) {
  if (err) {
    return console.log(err);
  }
  console.log(data);
});

在本教程中,我们不想太深入地讨论Node,但希望这类代码现在应该更容易阅读了。

同步与异步回调

一个回调是同步执行还是异步执行取决于调用它的函数。让我们看几个例子。

同步回调函数

当代码是同步的,它从上到下,逐行运行。操作一个接一个地发生,每个操作都等待前一个操作的完成。我们已经看到了Array中同步回调的一个例子。上面的约简函数。

异步回调函数

与同步代码相比,异步JavaScript代码不会一行一行地从上到下运行。相反,异步操作将注册一个回调函数,以便在它完成后执行。这意味着JavaScript解释器不必等待异步操作完成,而是可以在运行时继续执行其他任务。

异步函数的一个主要例子是从远程API获取数据。现在让我们看一个例子,了解它是如何使用回调的。

fetch('https://jsonplaceholder.typicode.com/users')
  .then(response => response.json())
  .then(json => {
    const names = json.map(user => user.name);
    names.forEach(name => {
      const li = document.createElement('li');
      li.textContent = name;
      ul.appendChild(li);
    });
  });

上面示例中的代码使用FetchAPI向一个伪JSON API发送一个请求,请求虚拟用户列表。一旦服务器返回响应,我们将运行第一个回调函数,该函数将尝试将响应解析为JSON。在此之后,运行第二个回调函数,它构造一个用户名列表并将它们追加到一个列表中。注意,在第二个回调函数中,我们使用另外两个嵌套回调函数来执行检索名称和创建列表元素的工作。

使用回调时要注意的事项

回调在JavaScript中已经存在很长一段时间了,它们可能并不总是最适合您想要做的事情。让我们看看需要注意的几件事。

小心JavaScript回调地狱

我们在上面的代码中看到,可以嵌套回调。这在处理相互依赖的异步函数时尤其常见。

虽然这对于一到两层嵌套是可以的,但是您应该意识到这种回调策略不能很好地扩展。用不了多久,你就会得到混乱且难以理解的代码:

fetch('...')
  .then(response => response.json())
  .then(json => {
    
    fetch('...')
      .then(response => response.json())
      .then(json => {
        
        fetch('...')
          .then(response => response.json())
          .then(json => {
            
            fetch('...')
              .then(response => response.json())
              .then(json => {
                
              });
          });
      });
  });

这被亲切地称为回调地狱,我们有一篇文章专门介绍如何避免这种情况:从回调地狱中拯救出来。

更喜欢更现代的流量控制方法

虽然回调是JavaScript工作方式中不可或缺的一部分,但该语言的最新版本增加了改进的流控制方法。

例如,承诺和async…Await为处理上述类型的代码提供了更清晰的语法。