Array.prototype.reduce方法是数组原型方法中比较强大的一个,在某些场景下,用其它方法可能要几步才能完成的,reduce甚至只用一行代码就能搞定。
举个最简单的例子,要求数组[1, 2, 3, 4, 5]
每一项的和,不用reduce的写法:
1 | const arr = [1, 2, 3, 4, 5] |
使用reduce来写:
1 | const arr = [1, 2, 3, 4, 5] |
可以看到函数体精简到了一行,我们甚至不需要定义中间的暂时变量。下面简单说下用法。
用法
1 | arr.reduce(callback(acc, cur[, idx[, src]]) [, initialValue]) |
- 该方法接收两个参数:回调函数callback和初始值initialValue,第二个参数是可选的。
- 回调函数callback接收4个参数:
- 累加器acc
- 数组迭代过程中的当前项cur
- 数组迭代过程中的当前索引idx
- 源数组
回调函数的后三个参数比较好理解,就和数组的forEach
、map
等方法的callback参数一致,重点是要理解累加器acc
和初始值initialValue
。
调用reduce
时会遍历数组并调用callback
,每一轮遍历callback
计算出来的返回值就作为一下轮遍历的累加器。
第一轮遍历和最后一轮遍历比较特殊,需要特别关注。最后一轮遍历的返回结果自然就作为reduce()
的返回值了。
而第一轮遍历的累加器从哪来呢?这就要说到初始值了,如果传了初始值,那初始值就作为第一轮遍历的累加器,并且数组是从索引0开始遍历的(此时acc === inititalValue;cur === src[0]
)。如果没传初始值,那数组的第0项就是第一轮遍历的累加器,同时数组是从索引1开始遍历的(此时acc === src[0];cur === src[1]
)。所以有初始值会比无初始值多一轮遍历。
用一个表格来列出每一轮遍历各个值是什么可以帮助我们更清晰地认识这个方法地执行过程,以上面数组求和地方法为例:
acc | cur | idx | src | returnValue |
---|---|---|---|---|
1 | 2 | 1 | [1,2,3,4,5] | 3 |
3 | 3 | 2 | [1,2,3,4,5] | 6 |
6 | 4 | 3 | [1,2,3,4,5] | 10 |
10 | 5 | 4 | [1,2,3,4,5] | 15 |
如果给上面的代码提供一个初始值:
1 | const sum = (arr) => arr.reduce((acc, cur) => acc + cur, 10) |
acc | cur | idx | src | returnValue |
---|---|---|---|---|
10 | 1 | 0 | [1,2,3,4,5] | 11 |
11 | 2 | 1 | [1,2,3,4,5] | 13 |
13 | 3 | 2 | [1,2,3,4,5] | 16 |
16 | 4 | 3 | [1,2,3,4,5] | 20 |
20 | 5 | 4 | [1,2,3,4,5] | 25 |
使用场景
了解了用法,我们来看看哪些场景下可以使用reduce
用reduce实现数组原型的其它方法
上面提到过,reduce
的callback
的后3个参数和forEach
、map
的callback
参数一致,他们是如此的相似,实际上forEach
、map
等这些数组方法都可以用reduce
来实现:
1 | // 用reduce实现forEach |
1 | // 用reduce实现map |
其它方法也是类似的思路,就不展开了。实现的过程需要注意:必须提供初始值[],因为不提供初始值遍历会从索引1开始。另外要注意要实现的方法的返回值和是否需要改变原数组。
数组去重
1 | const arr = [1, 1, 2, 2, 3, 3] |
二维数组转一维
1 | [[1, 2], [3, 4], [5, 6]].reduce((acc, cur) => acc.concat(cur), []) // [1, 2, 3, 4, 5, 6] |
计算最小值最大值
1 | [{value: 1}, {value: 2}, {value: 3}].map(el => el.value).reduce((acc, cur) => Math.max(acc, cur), -Infinity) |
这里有两点需要注意:
- 基本思路是用累加器和每一项比,用
Math.max
求出每一轮遍历的最大值,最后一轮算出的返回值就是最大值。同时因为要算的是最大值,所以初始值是-Infinity
- 中间要用
map
转一下是为了保证累加器在每一轮计算出的结果的类型保持一致。
计算数组中每个元素出现的次数
1 | [1,2,3,4,1,3,1].reduce((acc, cur) => { |
斐波那契数列
斐波那契数列由0
和1
开始,后面的每一项数字都是前面两项数字的和,形如0,1,1,2,3,5,8,13...
。这也是leetcode上的第509题,不过它的题目内容是求第N项是多少,比如F(0) === 0
,F(1) === 1
,F(2) === 1
。
如果是求前N项斐波那契数列组成的数组用reduce
可以很容易的实现:
1 | function fibonacci(n) { |
这里有以下几个关键点:
- 第0项和第1项是固定的直接返回
- 从第2项开始使用
reduce
,并且初始值是第1项的[0, 1]
- 解释下
const arr = Array(n g- 1).fill(1)
,由于前两项已经固定了,所以使用reduce
需要构造一个剩下项数位的数组,为什么是n - 1
,自己在心里用临界值推算下就知道了。而为什么要fill(1)
呢?这就是个小小的知识点了,因为Array(n)
构造出来的数组虽然length
是n
,但是每一项都是空(在chrome打印出来是[empty x n]
)是不参与遍历的,必须填充一个实际的值才能遍历这个数组,填充什么值是无所谓的。 reduce
函数体的内容用n === 2
推算下就知道了。当n === 2
时,累加器是[0, 1]
,现在要做的是,向累加器内push 1
,这个1
怎么得到的呢?就是前一项的[0, 1]
的最后两项的和即acc[acc.length - 1] + acc[acc.length - 2]
。
总结
reduce
的用法远不止于此,用好了可以大大的简化代码。我总结了下,发现当你你要做的事是要遍历一个数组,并且上一轮的遍历的结果会传递到下一轮遍历的过程中去的时候,基本都能使用reduce
来解决,和递归的思想有点像,又好像不完全一样。