JavaScript 的 Reduce 方法
直到最近,我唯一成功“精简”的东西就是我自己——精简到泪流满面。就像世界亿万富翁们最近纷纷踏上太空之旅,试图把他们富有的屁股发射到外太空一样,我也踏上了一段史诗般的冒险之旅,去理解 reduce 方法。你想和我一起踏上这段旅程,彻底、一劳永逸地理解这个臭名昭著、令人畏惧、独一无二的 reduce 方法吗?太好了。欢迎登上火箭 JavaScript 号。🚀
什么是reduce方法?
JavaScript 内置了许多数组方法,旨在简化我们的编程工作。这些方法提供了开箱即用且常用的功能,用于以特定方式遍历或操作数组。虽然这些方法数量不少,完全没有必要全部记住,但最好对它们的使用方法有所了解。
根据MDN的描述,`reduce()` 方法会对数组中的每个元素执行一个回调函数(由你提供),最终得到一个输出值。除了回调函数之外,它还可以接受一个初始值。
//reducer is the callback function, initialValue is the optional second param
array.reduce(reducer [, initialValue])
reducer 函数
回调函数接受四个参数,但根据具体需求,通常可以省略最后两个参数。该函数会应用于数组中的每个元素,最终返回一个值。
- 累加器- 用于累加 reducer 函数的返回值
- 当前值- 当前正在处理的元素
- 当前索引(可选) - 当前正在处理的元素的索引
- 源数组(可选)——我们要对其调用 reduce 方法的数组。
function reducer(accumulator, currentValue, currentIndex, array){}
这听起来可能很令人困惑,所以让我们来分解一下,并检查一下语法。
假设我们要编写一个函数,将数组中的所有元素相加并返回它们的和。我们要求和的初始数组如下所示。我们先忽略它显然加起来等于 10 的事实,假设我们的数学能力很差,以至于我们需要用编程的方式来解决我们认为不可能完成的数值难题。
const arr = [1,2,3,4]
现在我们来看看如何应用 reduce 方法。
//define the reducer function, provide it with its first 2 parameters
//returns the sum of the accumulator and currentValue
const calculateSum = (accumulator, currentValue) => accumulator + currentValue
//apply reducer function to array
arr.reduce(calculateSum)
上面我们告诉 reducer 函数返回累加器中的值与当前正在处理的值之和。这意味着当 reducer 遍历数组时,每个新数字都会被添加到累加器中不断增加的总和里。还是有点困惑?我承认。让我们添加一些 console.log 来了解这个过程是如何执行的。
解释
在文章中,我会用图片展示回调函数的累加器和当前值是如何变化的。然后我会用文字解释图片,这些文字可能对你有帮助,也可能没用。如果你是视觉型学习者,你可能会觉得图片本身更容易理解,而文字反而会让你感到困惑。如果你觉得某些部分与你的学习方式不符,可以随意跳过。
const calculateSum = (accumulator, currentValue) => {
console.log('accumulator: ', accumulator);
console.log('currentValue:', currentValue);
return accumulator + currentValue;
};
arr.reduce(calculateSum)
- 在第一次迭代中,累加器是数组的第一个元素,即 1。当前值(或正在处理的元素)是下一个元素,即 2。当我们对 2 应用 reducer 函数时,reducer 返回累加器 1 和当前值 2 的和。
- reducer 的返回值 3 成为新的累加器。currentValue 移动到数组中的下一个元素,恰好也是 3。将累加器加到 currentValue 上的函数应用于 currentValue 为 3 的情况,即 3 + 3,结果为 6。
- 因此,6 成为新的累加器。数组中的下一个元素,即当前值,现在是 4。将累加器和当前值相加的 reducer 现在应用于 4。6 + 4 = 10,由于数组中没有更多元素,因此 10 成为最终返回值。
呼。看来,这个数组方法不仅难以理解,也很难描述。如果我的解释让你感到困惑,我建议你花些时间逐行分析图片。
注:顺便一提,这并非 reduce 方法在实际应用中的常见用法。如果我们只是想对一个数字数组求和,完全可以使用 for 循环或 forEach 语句。不过,这样使用 reduce 可以很好地说明该方法的工作原理。本文后续还会介绍一些类似的“反例但解释得好”的例子。
初始值
我们还可以通过传入可选参数 initialValue,告诉我们的 reduce 方法将累加器初始化为我们自己选择的任意值。
arr.reduce(reducer, initialValue)
让我们再用上面的例子。
const arr = [1,2,3,4]
const calculateSum = (accumulator, currentValue) => {
console.log('accumulator: ', accumulator);
console.log('currentValue:', currentValue);
return accumulator + currentValue;
};
//here we tell the reduce method to initialise the accumulator at 10
arr.reduce(calculateSum, 10)
在之前的示例版本中,第一个累加器的值是 1,也就是数组的第一个元素。现在,我们通过向 reduce 方法添加第二个参数 initialValue 10 来覆盖这个值。10 现在成为我们的第一个累加器,reducer 也应用于数组中的第一个元素。
以下是对传入可选初始值参数如何影响 reduce 方法执行的总结。
| 初始值 | 累加器 | 当前值 |
|---|---|---|
| 未通过 | accumulator = array[0] |
currentValue = array[1] |
| 通过了 | accumulator = initialValue |
currentValue = array[0] |
将初始值设置为数字以外的值(例如空数组或对象)可以让我们对 reducer 进行一些巧妙的操作。让我们来看几个例子。
1. 使用 reduce 进行计数
假设我们要编写一个函数,该函数接收一个字符串作为参数,并返回一个包含该字符串字母个数的对象。如果我们的字符串是“save the bees”,我们期望的返回值将是:
{ s: 2, a: 1, v: 1, e: 4, " ": 2, t: 1, h: 1, b: 1 }
const string = "🚫🚫🚀🚀 less rockets, more bees pls"
const letterCountReducer = (acc, value) => {
acc[value] ? ++acc[value] : (acc[value] = 1);
return acc;
};
//the accumulator is initialised as an empty object
[...string].reduce(letterCountReducer, {})
解释
图示为上述过程执行顺序的开始。
- 因为我们传入了一个空对象的初始值,所以累加器被初始化为一个空对象。
- 遍历数组时,我们可以检查每个字母是否已作为键存在于累加器对象中。如果存在,则将其值加 1;如果不存在,则将其初始化为 1。
- 我们返回新的累加器,它现在包含了我们刚刚遍历过的字母,然后继续。最终,我们将返回一个包含所有已遍历字母对象的累加器。
2. 使用 reduce 函数展平数组
假设我们有一个数组的数组。三种动物,渴望在一起,却被坚不可摧的数组墙隔开。
//BOO! An unnatural habitat
const zoo = [
['🐇', '🐇', '🐇'],
['🐷', '🐷', '🐷'],
['🐻', '🐻', '🐻'],
];
我们该如何解救他们?
const flatten = (acc, animalArray) => acc.concat(animalArray);
zoo.reduce(flatten, []);
//returns ["🐇", "🐇", "🐇", "🐷", "🐷", "🐷", "🐻", "🐻", "🐻"]
//YAY! A natural habitat!
解释:
- 我们提供一个空数组作为累加器。
- reducer 将第一个当前值(这里命名为 animalArray)连接到空的累加器。我们返回这个新数组,现在其中包含了 3 只兔子。
- 这便成为新的累加器,接下来我们将下一个当前值(即动物数组)连接到它上面。原始数组中的第二个元素是一个猪数组。我们返回由兔子和猪组成的新累加器,然后继续处理熊。此时的累加器是一个由兔子和猪组成的数组。接下来,我们将当前值(即熊数组)连接到这个数组上。
注意:虽然这个例子是为了说明 reduce 方法的工作原理,但在实践中,我会选择 arr.flat() 方法,它正如其名称所示,可以执行扁平化操作。
3. 使用 reduce 函数对数组进行去重
假设我们有一个包含重复值的数组,而我们想要得到一个只包含唯一值的数组。
//initial arr
const arrOfDupes = ["🚀", "🚀", "🚀", "🌍"];
//desired output
["🚀", "🌍"];
const dedupe = (acc, currentValue) => {
if (!acc.includes(currentValue)) {
acc.push(currentValue);
}
return acc;
};
const dedupedArr = arrOfDupes.reduce(dedupe, []);
解释
- 我们从空数组的初始值开始,它成为我们的第一个累加器。
- 当 reduce 方法遍历数组时,回调函数会被应用于数组中的每个元素。它会检查累加器中是否已存在当前值。如果不存在,则将当前值添加到累加器中。
- 累加器将被返回,要么保持不变,要么添加一个额外的唯一值。
注意:虽然这个例子是为了说明 reduce 方法的内部工作原理,但在实践中,我会选择使用 Set 来对原始类型数组进行去重,这是一种性能更高的方法。
dedupedArr = [...new Set(array)];
4. 使用 reduce 对项目进行分组
假设我们要按属性对对象数组进行分组。我们从一个对象数组开始,最终得到一个包含两个数组的对象,这两个数组中的对象按选定的属性进行分组。
//initial array of objects to be grouped
const climateBehaviours = [
{ description: "Recycle", greenPoints: 30 },
{ description: "Cycle everywhere", greenPoints: 40 },
{ description: "Commute to work via plane", greenPoints: -70 },
{ description: "Replace beef with veg", greenPoints: 50 },
{ description: "Build a rocket for space tourism", greenPoints: -500 },
];
//desired output: an object with two groups
{
goodClimateBehaviours: [{}, {}, ...], // greenPoints >= 0
badClimateBehaviours: [{}, {}, ...], // greenPoints < 0
};
让我们来编写代码。
//reducer function
const groupBehaviour = (acc, currentObj) => {
currentObj.greenPoints >= 0
? acc.goodClimateBehaviours.push(currentObj)
: acc.badClimateBehaviours.push(currentObj);
return acc;
};
//initial value
const initialGrouping = {
goodClimateBehaviours: [],
badClimateBehaviours: [],
};
//applying the reduce method on the original array
const groupedBehaviours = climateBehaviours.reduce(groupBehaviour, initialGrouping);
对于马斯克、贝佐斯和布兰森这样的人来说,坏消息是,这就是我们最终的结局。
console.log(groupedBehaviours)
{
goodClimateBehaviours: [
{ description: "Recycle", greenPoints: 30 },
{ description: "Cycle everywhere", greenPoints: 40 },
{ description: "Replace beef with veg", greenPoints: 50 },
],
badClimateBehaviours: [
{ description: "Commute to work via plane", greenPoints: -70 },
{ description: "Build a rocket for space tourism", greenPoints: -500 },
],
};
解释
- 初始值是一个包含两个属性的对象,分别是 goodClimateBehaviours 和 badClimateBehaviours。这是我们的第一个累加器。
- 回调 reducer 函数遍历对象数组。每次遍历时,它都会检查当前对象的 greenPoints 值是否大于 0。如果大于 0,则将该对象添加到 accumulator.goodClimateBehaviours 中;否则,将该对象添加到 accumulator.badClimateBehaviours 中。最后,返回累加器。
- 最终返回值将是一个包含所有对象的累加器。
5. 使用 reduce 操作更复杂的数据结构
在实际应用中,reduce 函数最常用于处理复杂的数据结构。假设我们有一个对象数组,每个对象包含 id、description 和 outcomes 三个数组,其中每个 outcomes 可以是期望的,也可以是不期望的。我们希望将这个数组转换成一个结构截然不同的单一对象。
const climateActions = [
{
id: 'space_tourism',
description: 'build rockets for space tourism',
outcomes: [
{ outcome: 'rich people can go to space', isDesirable: false },
{ outcome: 'is pretty cool', isDesirable: true },
{ outcome: 'increased emissions', isDesirable: false },
{
outcome: 'investment diverted from green energy to space tourism',
isDesirable: false,
},
],
},
{
id: 'trees_4_lyf',
description: 'stop burning down the amazon',
outcomes: [
{ outcome: 'air for all', isDesirable: true },
{ outcome: 'our kids might live', isDesirable: true },
{
outcome: 'reduce threat of imminent extinction',
isDesirable: true,
},
{
outcome: 'make greta happy',
isDesirable: true,
},
{
outcome: 'make bolsonaro sad',
isDesirable: false,
},
],
},
];
我们的目标是将此数组转换为一个以 id 为键的单一对象,以及一个包含好结果和坏结果数组的对象,如下所示。
const climateInitiatives = {
'space_tourism': {
badOutcomes: [
'rich people can go to space',
'increased emissions',
'investment diverted from green energy to space tourism',
],
goodOutcomes: ['is pretty cool'],
},
'trees_4_lyf': {
badOutcomes: ['make bolsonaro sad'],
goodOutcomes: [
'air for all',
'our kids might live',
'reduce threat of imminent extinction',
'make greta happy',
],
},
};
以下是使用 reduce 实现此转换的一种方法。
const reducer = (acc, currentObj) => {
const newAcc = {
...acc,
[currentObj.id]: { badOutcomes: [], goodOutcomes: [] },
};
currentObj.outcomes.map(outcome => {
outcome.isDesirable
? newAcc[currentObj.id].goodOutcomes.push(outcome.outcome)
: newAcc[currentObj.id].badOutcomes.push(outcome.outcome);
});
return newAcc;
};
const res = climateActions.reduce(reducer, {});
我们也可以使用嵌套的 reduce 方法来代替 map 方法,但这样做可能会破坏矩阵结构。🤯
解释
- 第一个累加器是一个空对象。当前值(这里称为当前对象)是原始数组中的第一个对象。
- reducer 函数初始化一个新变量 newAcc。newAcc 是一个对象,包含当前(仍然为空)累加器的扩展值。我们为 newAcc 赋一个新属性,该属性的键是当前对象的 id,值是一个包含好坏结果数组的对象。
[currentObj.id]: { badOutcomes: [], goodOutcomes: [] } - 然后,我们遍历当前对象的结果数组,并根据结果是否令人满意,将其推入新变量 newAcc 的结果数组中。
- 我们返回 newAcc,它将成为下一轮迭代的 acc,因此当我们展开它时,我们不会丢失它的内容。
结论
我们学到了什么?希望是减少的方法(而且我显然不太喜欢亿万富翁们把资源浪费在自私地追求太空旅行上,而我们都应该集中精力防止灾难性的全球变暖,但这只是我的个人观点🔥)。
Reduce 无疑是 JavaScript 内置方法中比较棘手的一个。但就像大多数编程一样,真正理解它的最佳方法就是实践。如果本文中的示例让你理解了——太好了!如果没理解——也很好,这是你再次尝试和练习的机会,直到你完全掌握为止。我保证,你最终一定会明白的。
现在,让我们来减少一些代码吧。也减少一些排放。🍃
文章来源:https://dev.to/sanspanic/the-javascript-reduce-method-3j8l







