原文:How the JavaScript reduce and reduceRight Methods Work,作者:Ashutosh Biswas

reducereduceRight是JavaScript内置的两个数组方法,这两个方法的学习曲线略微陡峭。

其实这两个方法的本质就像下面的算术题一样简单。

假设我们有一个数字数组:

[1, 2, 3, 4]

我们需要对这个数组求和。

使用reduce求和的方法类似于:

((((1) + 2) + 3) + 4)

而使用reduceRight求和的方法类似于:

((((4) + 3) + 2) + 1)

使用reducereduceRight时,你可以定义你自己的“+”。数组中的元素也可以替换成任意值。挺激动人心的,不是吗?

不用把reducereduceRight看得太复杂,其实它们就相当于上面算术题的概括。在这篇文章我们会讲解这两个方法的重要信息。

本文将列举易于理解的算法例子来演示JavaScript中的归约(reduction)。

就这个话题我还制作了视频,如果你喜欢通过视频学习新知识的话,可以点击观看。

目录

归约是什么

你可能会好奇:“当我们使用reducereduceRight的时候,我们使用了那种归约?”

这里归约指的是一种把数组里的元素转化(后文会有更详细的介绍)成单一值的特定方法,就如同上面的算术题一样。

但需要注意的是,输出值可以为任意值。也就是说当在数组上使用归约之后,输出值可能比原始值要大。

函数式编程 语言中, 归约有 各种个样的别名 如:折叠(fold)累加(accumulate)聚合(aggregate)压缩(compress),甚至注入(inject)

`reduce`/`reduceRight`的参数

这两个方法的调用规则相同,可以放在一起学。让我们看看它们是怎么被调用的:

let myArray      = [/* 一个数组 */];
let callbackfn   = /* 一个函数值 */ ;
let initialvalue = /* 任意值 */ ;

myArray.reduce(callbackfn)
myArray.reduce(callbackfn, initialValue)

myArray.reduceRight(callbackfn)
myArray.reduceRight(callbackfn, initialValue)

reduce/reduceRight 的参数有:

callbackfn:必须是一个函数。 reduce/reduceRight在遍历数组的时候,会在每一个元素上调用 callbackfn。这函数有四个参数,我们假设变量 previousValuecurrentElementindexarray为这四个参数。所以callbackfn的内部调用情况如下:

callbackfn(previousValue, currentElement, index, array)

让我们看看这四个值的含义:

  1. previousValue,也被称作 累加器(accumulator),简单讲就是这个值代表了方法返回值的“当前状态”,这个值是由什么组成的,等你学完后面的演算规则就会完全明白
  2. currentElement,当前元素
  3. index,当前元素的索引
  4. array,一开始声明的myArray

返回值:当最后一次调用callbackfn时,返回值就会成为reduce/reduceRight的返回值。 其他情况下, 返回值会赋值给 previousValue以便下次调用callbackfn

initialValue(初始值):这是 previousValue(累加器)的一个可选值。 如果存在,且 myArray中包含元素, 首次调用 callbackfn时会将它视为 previousValue传入。

注意callbackfn通常被称为 reducer函数(或简写为reducer)。

通过图表理解`reduce`/`reduceRight`

reducereduceRight的唯一区别是遍历的顺序。reduce从左到右遍历,reduceRight从右到左遍历。

让我们看看如何使用 reduce/reduceRight将数组元素连接成字符串。注意是怎么一步一步分别从两个方向连接元素实现最终输出值的。

“展示reduce和reduceRight区别的图表”
展示reduce和reduceRight区别的图表

注意:

  • acc用来访问previousValue
  • curVal 用来访问 currentElement
  • 指向 r 的圆圈输入代表curVal
  • 指向 r 的长方形输入代表acc或者累加器。
  • 长方形中是初始值,因为它们作为acc传入**_r_**

`reduce`/`reduceRight`的演算规则

下面的29行算法代码乍一看可能让人生畏。但你会发现理解他们比理解上述解释性的句子要容易得多。

所以放轻松,享受这些步骤,别忘了可以在控制台实践这些步骤:

  • 1 If initialValue存在

    • 2 If myArray没有元素
      • 3 Return initialValue
    • 4 Else
      • 5 将initialValue赋值给accumulator
      • 6 If 方法是reduce,
        • 7 startIndex即为myArray最左端元素的索引
      • 8 If 方法是reduceRight,
        • 9 startIndex即为 myArray最右端元素的索引
  • 10 Else

    • 11 If myArray没有元素
      • 12 抛出TypeError
    • 13 Else if myArray只有一个元素
      • 14 Return 这个元素
    • 15 Else
      • 16 If 方法是 reduce,
        • 17 accumulator即为myArray最左端的元素
        • 18 startIndex即为myArray最左端元素后一位元素的索引
      • 19 If 方法是reduceRight,
        • 20 accumulator即为myArray最右端的元素
        • 21 startIndex即为myArray最右端元素前一位元素的索引
  • 22

  • 23 If 方法是reduce,

    • 24 从左到右遍历myArray, 每一个元素的索引 istartIndex
      • 25 将 accumulator带入 callbackfn(accumulator, myArray[i], i, myArray)求值
  • 26 If 方法是 reduceRight,

    • 27 从右到左遍历myArray, 每一个元素的索引 istartIndex
      • 28 将 accumulator带入callbackfn(accumulator, myArray[i], i, myArray)求值
  • 29 返回 accumulator

注意:数组的长度大于 0但是没有元素。数组的这些空的部分通常被称作空位(hole)。举个例子:

let arr = [,,,,];
console.log(arr.length);
// 4

// 注意尾部的逗号不会增加数组长度;
// 这个特征可以帮助我们更快添加新元素

reduce和reduceRight仅对 myArray中真实存在的元素调用callbackfn。 例如,你的数组是 [1,,3,,5]回调函数不会考虑没有元素的索引13。猜一猜下面的代码会打印什么内容:

[,,,3,,,4].reduce((_, cv, i) => {
  console.log(i);
});

如果你的答案是6,你是对的!

⚠️ 注意:不建议使用callbackfn来修改 myArray,因为这样会复杂化代码,容易产生bug。

如果你到目前为止都理解的话,那么恭喜你已经了解 reduce/reduceRight的运行方式了。

现在就是用 reduce/reduceRight解决问题最好的时候。 在看答案之前,最好花点时间自己做一下或者尝试思考一下。

练习

扁平化嵌套数组

编写一个flatten函数,扁平化嵌套数组:

let arr = [1, [2, [3], [[4], 5], 6]];
console.log(flatten(arr));
// [1, 2, 3, 4, 5, 6]

答案

const flatten = (arr) => 
  arr.reduce((acc, curVal) =>
    acc.concat(Array.isArray(curVal) ? flatten(curVal) : curVal), []);

数组去重

编写函数 rmDuplicates删除下列数组中重复的元素:

console.log(rmDuplicates([1, 2, 2, 3, 4, 4, 4]));
// [1, 2, 3, 4]

答案

const rmDuplicates = arr => 
  arr.reduce((p, c) => p.includes(c) ? p : p.concat(c), []);

在不改变原数组的情况下反转数组

虽然数组有内置方法reverse来反转数组,但会改变原数组。使用reduceRight在不改变数组的情况下,实现反转:

答案

let arr = [1, 2, 3];

let reversedArr = arr.reduceRight((acc, curVal) => [...acc, curVal], []);

console.log(arr);
// [1, 2, 3]

console.log(reversedArr);
// [3, 2, 1]

注意这样改变数组会丢失数组中所有的空位。

总结

reduce/reduceRight 内部调用 callbackfn时,我们将这种行为称作“常规行为”,其他行为就是边界案例,可以概括如下表格:

初始值 元素数量 输出
存在 0 边界案例:初始值
存在 大于 0 常规行为
不存在 0 边界案例:TypeError
不存在 1 边界案例:该元素
不存在 大于 1 常规行为

学习 reduce/reduceRight会比其他高阶数组方法更花时间,但它们是值得花时间学习的。

感谢阅读,希望这篇文章对你有帮助!你可以访问我的网站或者在 TwitterLinkedIn 关注我!

归约愉快!😃