你对Array.prototype.reduce究竟有多了解?

“ 做前端这么久了,想必大家肯定对数组原型链上面的reduce或多或少有了解,其实最开始学习前端的时候对reduce可能就停留在求和这一类的思考上,今天我就来带领大家走进reduce的世界,reduce能做的事情可真的多,唉呀妈呀真香”

1. reduce的回顾和它原理

  1. 首先我们先来回顾一下reduce的用法。

reduce接收两个参数:

(1)第一个参数是 callback 回调函数(必填);

callback回调函数中又会接收四个参数:(其实在日常学习、开发中使用到最多的是前两个参数,即acc累计器和cur当前值)

a. Accumulator (acc) (累计器)

b. Current Value (cur) (当前值)

c. Current Index (idx) (当前索引)

d. Source Array (src) (源数组)

(2)第二个参数是初始值initialValue,作为第一次调用 callback函数时的第一个参数的值。如果没有提供初始值,则将使用数组中的第一个元素。注意:在没有初始值空数组上调用 reduce 将报错

  1. reduce的运行过程:

callback回调函数的返回值分配给累计器acc,该返回值在数组的每个迭代中被记住,并最后成为最终的单个结果值。

  1. 举两个例子:
    1
    2
    3
    4
    5
    6
    // 不使用reduce第二个参数initialValue赋初始值,那么reduce会从index为1的地方开始执行回调
    [1,2,3,4].reduce((acc, cur) => acc + cur); // 10

    // 使用reduce第二个参数initialValue赋初始值,那么reduce会从index为0的地方开始执行回调
    [1,2,3,4].reduce((acc, cur) => acc + cur, 5); // 15
    MDN wiki
    MDN wiki

reduce内部编写原理:
在回顾了reduce的用法之后我们就来剖析一下reduce究竟是怎么实现的。
(1)首先需要判断一下给reduce中传递的第一个回调函数cbs是否是函数;
(2)然后reduce中第二个参数initVal是否传递有值,用一个变量acc去接收,如果第二个参数有值就去该值,否则就取数组中index为1的值;
(3)接着再定义一个startIndex变量用来判断for循环是从0开始,还是从1开始,当initVal有值时startIndex为0,否则为1;
(4)for循环中利用cbs每次return回来的值来更新acc;
(5)当for循环结束之后就返回acc的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 在这里不要使用箭头函数,否则this会指向window而不是指向数组本身
Array.prototype.myReduce = function (cbs, initVal) {
if (typeof cbs !== 'function') {
throw new TypeError('cbs is not a function');
}
let acc = initVal || this[0];
const startIndex = initVal ? 0 : 1;
for (let i = startIndex; i < this.length; i++) {
acc = cbs(acc, this[i], i, this);
}
return acc;
};
[1,2,3].myReduce((acc, cur, index) => acc + cur, 5); // 11

在实现reduce方法的时候需要注意this的指向问题,在外层不要使用箭头函数,否则this不会指向数组本身而是指向window对象。
在了解并自己编写reduce方法之后,我们就更加明白reduce的使用了,接下来的我会带领大家用reduce来玩转map、forEach等方法的实现。

2. reduce编写forEach

首先你得知道forEach的使用,在这里我就不多介绍它的使用了,在这类forEach第一个参数接收一个callback回调函数,回调函数中接收三个参数分别是item当前元素、index当前元素索引、arr原数组。接下来就是使用reduc来实现forEach方法,具体代码如下所示:

1
2
3
4
5
6
7
8
9
const arr = [1,2,3,4,5];
// 注意在这里不能使用箭头符号,否则this是指向window而不是arr
Array.prototype.reduceForEach = function (cbs) {
return this.reduce((acc, cur, index, arr) => {
cbs(cur, index, arr);
// 这里需要添加[],否则会从index为1开始计算,而不是从index为0开始(其实就是起到填充的作用)
}, []);
};
arr.reduceForEach((item, index) => console.log(item, index));

在编写的时候需要注意的就是this的指向问题,还有就是需要给reduce函数传递第二个参数,作用的作为填充,能让cur从下标index为0开始,而不是作为1开始计算。

3. reduce编写map

在02中我们尝试用reduce编写forEach方法,我们来使用reudce编写map,其实大同小异,map和forEach的不同点在于map会有return返回值。接下来就是放reduce编写map的代码时刻,具体代码如下所示:

1
2
3
4
5
6
7
8
9
10
const arr = [1,2,3,4,5];
Array.prototype.reduceMap = function (cbs) {
return this.reduce((acc, cur, index, arr) => {
const item = cbs(cur, index, arr); // 获取到每次改变值
acc.push(item); // 将每次改变值存储到acc累计器中
return acc;
// 需要给acc赋初始值为空数组,否则会报错
}, []);
};
arr.reduceMap((item, index) => item + index); // [1, 3, 5, 7, 9]

在编写时需要注意reduce第二个参数需要接收一个初始值,为空数组,然后我们需要存储每一次当前元素改变之后的值,然后通过push到数组中进行一个存储,最后再返回即可。

4. reduce编写filter

在使用reduce写了forEach和map两个方法之后,是不是感觉开始顺手了呢?那么趁现在趁热打铁,我们继续reduce来编写filter方法。具体代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
const arr = [1,2,3,4,5];
Array.prototype.reduceFilter = function (cbs) {
return this.reduce((acc, cur, index, arr) => {
const item = cbs(cur, index);
if (item) {
// item存在证明匹配上,那么就需要把匹配上的当前值存到累计器中
acc.push(cur);
}
// 由于filter即使你什么都没有匹配上,还是会返回[],所以最后我们需要返回acc累计器
return acc;
}, []);
};
arr.reduceFilter((item, index) => item % 2 !== 0); // [1,3,5]

在编写filter方法时,我们需要注意到的是在最后无论匹配还是没有匹配上都需要一个返回值,这个返回值就是acc累计器,因为filter方法是即使你没有匹配上也会返回一个空数组。如果匹配上的话就将当前匹配上的cur当前值push存储到acc累计器中。

5. reduce编写find

使用reduce编写find方法可能会有一些难度,但是当你掌握上面几种方法的变换写法之后,再来写find方法也是得心应手。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Array.prototype.reduceFind = function (cbs) {
return this.reduce((acc, cur, index, arr) => {
const findItem = cbs(cur, index, arr);
// 在数组中找到第一个元素,此时acc为空数组应该将其置为cur当前元素值
if (findItem) {
if (acc instanceof Array && acc.length === 0) {
acc = cur;
}
}
// 将数组整个都循环完没有找到,此时acc应该为空数组[],这时需要将acc置为undefined
if ((index === arr.length - 1) && acc instanceof Array && acc.length === 0) {
acc = undefined;
}
return acc;
}, []);
};
arr.reduceFind((item, index) => item % 2 !== 0); // 1

其实在工作中reduce真的有超级多的用处,就比如我们常说的二维数组变一位数组、数组去重等。不得不说从会用reduce到熟悉掌握之后,你就会说真香呀,啊哈哈哈。今天就介绍到这里,是不是对reduce的原理以及reduce的相关扩展有了更近一步的了解了呢。如果你觉得很赞或者对你有帮助的话请三连哟。