我最近在指导一位在 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
,它只在开始时使用。接下来,我们可以创建一个函数,该函数接受一个累加器和一个项目。累加器是在上次调用中返回的收集值,它告知函数下一个值将添加到什么。在这种加法的情况下,您可以将其想象成一个滚下山坡的雪球,随着它吞噬路径上的每个值,它的体积会随着每个被吞噬的值而增加。

我们将使用 .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"
以下是更直观的分解:
- 它显示累加器从我们的初始值
0
开始 - 然后我们有第一个项目,它是 1,因此我们的返回值是
1
(0 + 1 = 1
) 1
成为下一次调用的累加器- 现在我们有
1
作为累加器,并且 3 是项目,因为它在数组中排在下一位。 - 返回值变为
4
(1 + 3 = 4
) - 依次,它成为累加器,并且下一次调用的下一个项目是
6
- 这将导致
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 也做得很好。
我想知道是否有可能展示不使用 .reduce() 的方法,然后展示性能差异。我想知道当人们面对这两种不同的选择时,如何直观地理解代码的用途。
嗨,David!性能方面是一个很好的话题,因为你直觉是对的——在某些情况下,函数式以及
.reduce()
的性能并不理想,并且不是正确的工具。我看看是否能找到一些说明性的例子。不过,基准测试的问题在于,它们往往具有一些模糊的逻辑——确切的实现确实很重要,因此可能会误导人们认为在所有情况下,一种方法总是优于另一种。很好的观察!我喜欢这个视频——可视化 reducers 工作原理的好方法!
与 for 循环相比,reducers 的另一个优势是它们的可读性可能更高,尤其是在命名时。
使用你的
sum
示例,这是一个很好的观点,Adam!
我想附和一下关于展示reduce工作原理的视频的观点。它们很棒。对于那些学习风格更偏向视觉的人来说,这真的可以帮助他们理解所呈现的概念。感谢您抽出额外的时间制作这些视频。
我同意Adam!可读性非常好。我最喜欢的编写方式就像您在这里所做的那样……将reducer函数作为命名函数传递给
[].reduce()
可读性很重要
谢谢Sarah,这让我理解了reduce方法,到目前为止我还没有怎么用过它。查看MDN文档,使用它来展平数组将非常有用
展平是reduce的一个很好的用途——目前还有一个提案要添加
flat()
方法。https://mdn.org.cn/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat不得不承认我唯一的问题是为什么!“for循环”非常明显,它说明了一切。
我想这实际上只是一个lambda函数,所以为什么不叫它lambda函数呢。即使那样,对于这样的事情来说,lambda函数也有些过头了。
也许只是我,但语言(不仅仅是JS)变得越来越复杂,却没有真正好的理由或实际的好处!
但我想这可以让工具和教育者有工作,以及错误猎手,因为新的工具/功能会被用户搞砸。
或者也许我只是一个老派的反动派:)
尽管如此,很好的解释——谢谢
如果你不喜欢函数式编程,你当然不必使用它。
您提到了其他文章,我已经读过了,但您已经用我能够理解的术语将其简化了。