掌握 JavaScript 的难点:回调 I

2025-05-27

掌握 JavaScript 的难点:回调 I

我目前正在学习Frontend Masters 的《JavaScript:难点 v2》课程。这门课非常精彩,由 Will Sentance 主讲。课程涵盖了以下几个关键概念:

  1. 回调和高阶函数
  2. 闭包(范围和执行上下文)
  3. 异步 JavaScript 和事件循环
  4. 类和原型(OOP)

在本系列教程中,我将讲解每个部分的练习,提供我自己的解决方案,并附上我是如何得出该解决方案的说明。第一部分将讨论回调。

回调是 JS 中一个固有的基本概念,因为从闭包到异步 JavaScript 的大多数内容都建立在它们之上。在接触 JS 之前,我从未遇到过高阶函数(可以将另一个函数作为输入,或返回一个函数的函数),所以一开始我觉得这个概念非常令人困惑。谢天谢地,经过大量的练习,我能够很好地掌握回调。我鼓励你先实现自己的解决方案,然后再看我的,然后进行比较和对比。当然,解决这些练习的方法有很多种,我的绝对不一定是最好的。我的解决方案都在 GitHub 上,非常欢迎你 fork 这个仓库自己研究,或者,如果你找到了更好的解决方法,请发送 PR。

如果你是 JS 新手,或者对回调函数感到困惑,我认为这些练习能帮助你掌握这个概念。更多信息,可以在这里找到 Will 的课程幻灯片(PDF)。

练习 1

创建一个函数 addTwo,接受一个输入并将其加 2。

console.log(addTwo(3))应该输出5

console.log(addTwo(10))
应该输出12

解决方案 1

function addTwo(num) {
  return num + 2;
}

最简单的练习。它让我们感到很安心,因为我们知道自己知道如何使用函数。别担心,很快就会变得有趣!

练习 2

创建一个函数 addS,它接受一个输入并在其中添加一个“s”。

console.log(addS("pizza"));应该输出pizzasconsole.log(addS("bagel"));应该输出bagels

解决方案 2

function addS(word) {
  return word + "s";
}

另一个简单的函数。提醒一下,它+是 JS 中一个重载运算符,可以处理字符串数字。

练习 3

创建一个名为 map 的函数,该函数接受两个输入:
一个数字数组(一个数字列表)
一个“回调”函数 - 应用于数组每个元素的函数(在函数“map”内部)
让 map 返回一个新数组,其中填充的数字是使用“回调”函数对输入数组的每个元素进行操作的结果。

console.log(map([1, 2, 3], addTwo));应该输出[ 3, 4, 5 ]

解决方案 3

function map(array, callback) {
  const newArr = [];
  for (let i = 0; i < array.length; i++) {
    newArr.push(callback(array[i]));
  }
  return newArr;
}

现在更有趣了!我们基本上在这里重新实现了原生 Array.prototype.map() 函数的简单版本。我决定在这里使用一个基本的 for 循环,因为大多数人应该都熟悉它。我认为这可能是本系列中最重要的练习,如果你能理解这一点,你基本上就掌握了回调!

练习 4

函数 forEach 接受一个数组和一个回调,并在数组的每个元素上运行回调。forEach 不返回任何内容。

let alphabet = "";
const letters = ["a", "b", "c", "d"];
forEach(letters, function (char) {
  alphabet += char;
});
console.log(alphabet);

应该输出abcd

解决方案 4

function forEach(array, callback) {
  for (let i = 0; i < array.length; i++) {
    callback(array[i]);
  }
}

这是原生数组方法的另一个重新实现。注意它与 map 的区别:map 返回一个数组,而 forEach 不返回任何内容,所以所有需要发生的事情都必须在回调函数体中进行。

练习 5

重新构建你的 map 函数,这次不要使用 for 循环,而是使用你刚刚定义的 forEach 函数。将这个新函数命名为 mapWith。

console.log(mapWith([1, 2, 3], addTwo));应该输出[ 3, 4, 5 ]

解决方案 5

function mapWith(array, callback) {
  const newArr = [];
  forEach(array, (item) => {
    newArr.push(callback(item));
  });
  return newArr;
}

以这种方式使用你自己定义的函数非常强大。它能让你深入了解函数的具体工作原理。现在,当你使用 lodash 或 underscore 之类的库时,你就能想象底层函数是如何实现的。

练习 6

Reduce 函数接受一个数组,并将所有元素缩减为单个值。例如,它可以对所有数字求和、相乘,或者执行任何可以放入函数中的操作。

const nums = [4, 1, 3];
const add = function (a, b) {
  return a + b;
};
console.log(reduce(nums, add, 0))

应该输出8

解决方案 6

function reduce(array, callback, initialValue) {
  let accum;
  if (Object.keys(arguments).length > 2) {
    accum = initialValue;
  } else {
    // InitialValue not provided
    accum = array[0];
    array.shift();
  }

  forEach(array, (item) => {
    accum = callback(accum, item);
  });
  return accum;
}

啊,Reduce!JS 中最容易被误解却又最强大的函数之一(更广泛地说,在函数式编程中也是如此)。它的基本概念是:你有一个初始值,对数组中的每个元素运行回调函数,并将结果赋给这个初始值。最后,返回这个值。

Reduce 的另一个陷阱是 initialValue 参数是可选的,调用者可能会提供或不提供。如果提供了,我们应该使用它的值作为数组的初始累加器。如果没有提供,我们应该将数组的第一个元素视为累加器。这里我们通过检查来测试提供的参数数量,Object.keys(arguments).length并相应地设置累加器。

注意我们如何使用我们自己的 forEach 函数,当然我们也可以使用本机 array.forEach(),具有相同的行为。

编辑:感谢 Jason Matthews(在下面的评论中)指出我之前的解决方案(赋值initialValue给自身)可能会产生意想不到的副作用。通过赋值给一个新变量,我们让函数变成了纯函数。

编辑 2:感谢 Dmitry Semigradsky 发现了 reduce 实现中的一个错误!

练习 7

构造一个函数交集,用于比较输入数组,并返回一个包含所有输入元素的新数组。补充:使用 reduce!

console.log(
  intersection([5, 10, 15, 20], [15, 88, 1, 5, 7], [1, 10, 15, 5, 20])
);

应该输出[5, 15]

解决方案 7

function intersection(...arrays) {
  return arrays.reduce((acc, array) => {
    return array.filter((item) => acc.includes(item));
  });
}

将 reduce 和 filter 结合起来,可以得到一个强大的函数。这里,如果acc没有提供参数,它会被设置为第一个数组,而我们并没有将其作为参数提供。因此,在后续调用中,我们只需过滤数组,返回也包含在acc` 数组中的项即可。

注意的使用...arrays,这里我们使用剩余参数,因为我们不知道将向函数提供多少个参数。

文章来源:https://dev.to/internettradie/mastering-hard-parts-of-javascript-callbacks-i-3aj0
PREV
识别冒名顶替综合症并在它搞砸你的编码面试之前解决它你所知道的一切不断膨胀的宇宙其他人都知道这是一个焦点问题
NEXT
2020 年最佳静态网站生成器