理解万能的 Reducer

Avatar of Sarah Drasner
Sarah Drasner

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

我最近在指导一位在 JavaScript 中使用 .reduce() 方法遇到困难的人。具体来说,是如何从

const nums = [1, 2, 3]
let value = 0

for (let i = 0; i < nums.length; i++) {
  value += nums[i]
}

…到这个

const nums = [1, 2, 3]
const value = nums.reduce((ac, next) => ac + next, 0)

它们在功能上是等价的,并且都对数组中的所有数字求和,但它们之间存在一些范式转换。让我们花点时间来探索一下 reducers,因为它们功能强大,并且是您编程工具箱中不可或缺的一部分。关于 reducers 的文章数不胜数,我将在最后链接一些我最喜欢的文章。

什么是 Reducer?

关于 Reducer,首先也是最重要的一点是要理解**它始终只返回一个值**。Reducer 的作用就是缩减。这个值可以是数字、字符串、数组或对象,但始终只有一个。Reducers 非常适合许多用途,但它们尤其适用于对一组值应用一些逻辑并最终得到另一个单个结果。

还需要说明的是:本质上,reducers 不会修改您的初始值;相反,它们会返回其他内容。让我们看一下第一个示例,以便您了解这里发生了什么。**下面的视频解释了这一点**

观看视频可能有助于了解进度的发生方式,但以下是我们正在查看的代码

const nums = [1, 2, 3]
let value = 0

for (let i = 0; i < nums.length; i++) {
  value += nums[i]
}

我们有我们的数组 (1, 2, 3) 以及数组中每个数字将加到其上的第一个值 (0)。我们遍历数组的元素,并将它们添加到初始值中。

让我们尝试以稍微不同的方式来做这件事

const nums = [1, 2, 3]
const initialValue = 0

const reducer = function (acc, item) { 
  return acc + item
}

const total = nums.reduce(reducer, initialValue)

现在我们有相同的数组,但这次我们没有修改第一个值。相反,我们有一个 initialValue,它只在开始时使用。接下来,我们可以创建一个函数,该函数接受一个累加器和一个项目。累加器是在上次调用中返回的收集值,它告知函数下一个值将添加到什么。在这种加法的情况下,您可以将其想象成一个滚下山坡的雪球,随着它吞噬路径上的每个值,它的体积会随着每个被吞噬的值而增加。

snowball accumulating values

我们将使用 .reduce() 来应用函数并从该初始值开始。这可以用箭头函数缩短

const nums = [1, 2, 3]
const initialValue = 0

const reducer = (acc, item) => { 
  return acc + item
}

const total = nums.reduce(reducer, initialValue)

然后可以进一步缩短!隐式返回大获全胜!

const nums = [1, 2, 3]
const initialValue = 0

const reducer = (acc, item) => acc + item

const total = nums.reduce(reducer, initialValue)

现在我们可以直接在调用它的地方应用函数,并且还可以直接将该初始值放在那里!

const nums = [1, 2, 3]

const total = nums.reduce((acc, item) => acc + item,

累加器可能是一个令人望而生畏的术语,因此您可以将其视为我们在回调的调用中应用逻辑时数组的当前状态。

调用栈

如果尚不清楚发生了什么,让我们记录下每次迭代中发生的情况。reduce 使用一个回调函数,该函数将对数组中的每个项目运行。以下演示将有助于更清楚地说明这一点。我还使用了不同的数组 ([1, 3, 6]),因为使数字与索引相同可能会令人困惑。

查看 CodePen 上 Sarah Drasner (@sdras) 的笔 显示 acc、item 和 return

当我们运行此代码时,将在控制台中看到以下输出

"Acc: 0, Item: 1, Return value: 1"
"Acc: 1, Item: 3, Return value: 4"
"Acc: 4, Item: 6, Return value: 10"

以下是更直观的分解:

  1. 它显示累加器从我们的初始值 0 开始
  2. 然后我们有第一个项目,它是 1,因此我们的返回值是 1 (0 + 1 = 1)
  3. 1 成为下一次调用的累加器
  4. 现在我们有 1 作为累加器,并且 3 是项目,因为它在数组中排在下一位。
  5. 返回值变为 4 (1 + 3 = 4)
  6. 依次,它成为累加器,并且下一次调用的下一个项目是 6
  7. 这将导致 10 (4 + 6 = 10),并且是我们最终的值,因为 6 是数组中的最后一个数字

简单示例

现在我们已经掌握了这些知识,让我们看看 reducers 可以做的一些常见且有用的事情。

我们有多少个 X?

假设您有一个数字数组,并且您希望返回一个对象,该对象报告这些数字在数组中出现的次数。请注意,这同样适用于字符串。

const nums = [3, 5, 6, 82, 1, 4, 3, 5, 82]

const result = nums.reduce((tally, amt) => {
  tally[amt] ? tally[amt]++ : tally[amt] = 1
  return tally
}, {})

console.log(result)

查看 CodePen 上 Sarah Drasner (@sdras) 的笔 简化的 reduce

等等,我们刚刚做了什么?

最初,我们有一个数组以及我们要将其内容放入其中的对象。在我们的 reducer 中,我们询问:此项目是否存在?如果是,让我们递增它。如果不是,则添加它并将其设置为 1。最后,请返回每个项目的计数总数。然后,我们运行 reduce 函数,传入 reducer 和初始值。

获取一个数组并将其转换为显示某些条件的对象

假设我们有一个数组,并且我们想根据一组条件创建一个对象。Reduce 非常适合此用途!在这里,我们想从数组中包含的任何数字实例创建对象,并显示该数字的奇数和偶数版本。如果数字已经是偶数或奇数,那么对象中就会包含该数字。

const nums = [3, 5, 6, 82, 1, 4, 3, 5, 82]

// we're going to make an object from an even and odd
// version of each instance of a number
const result = nums.reduce((acc, item) => {
  acc[item] = {
    odd: item % 2 ? item : item - 1,
    even: item % 2 ? item + 1 : item
  }
  return acc
}, {})

console.log(result)

查看 CodePen 上 Sarah Drasner (@sdras) 的笔 简化的 reduce

这将在控制台中输出以下内容

1:{odd: 1, even: 2}
3:{odd: 3, even: 4}
4:{odd: 3, even: 4}
5:{odd: 5, even: 6}
6:{odd: 5, even: 6}
82:{odd: 81, even: 82}

好的,那么发生了什么?

当我们遍历数组中的每个项目时,我们为偶数和奇数创建一个属性,并基于带有模运算符的内联条件,我们将存储数字或将其递增 1。模运算符非常适合此用途,因为它可以快速检查偶数或奇数——如果它能被 2 整除,则为偶数,否则为奇数。

其他资源

在开头,我提到了其他一些方便的资源文章,可以帮助您更熟悉 reducers 的作用。以下是我的一些最爱

  • IMO,MDN 文档 对此非常棒。说真的,这是他们最好的文章之一。他们还更详细地描述了如果未提供初始值会发生什么,这在本文中我们没有涉及。
  • Daniel Shiffman 总是能在 Coding Train 上以惊人的方式解释事物。
  • A Drip of JavaScript 也做得很好。