Array.prototype.reduce 实用指南
部分内容引自原文地址
在原文基础上有增删改。
Array.prototype.reduce
算是 JavaScript 数组中比较难用但又特别强大的方法。
本文第一部分以实用为主,通过例子展示如何使用这个方法。
第二部分介绍一下reduce方法的本质和异常处理。
一、简介 & 例子
Array.prototype.reduce 简介
reduce()
方法对累加器和数组中的每个元素(从左到右)应用一个函数,将其简化为单个值。
上述是 MDN对该方法的描述,方法的语法是:
arr.reduce(callback[, initialValue])
。
callback
接受四个参数,分别是:
accumulator,累加器累加回调的返回值;
currentValue,数组中正在处理的元素;
currentIndex(可选),数组中正在处理的当前元素的索引;
array(可选),调用 reduce()
的数组。
initialValue
为可选参数,作为第一次调用 callback
函数时的第一个参数的值。方法的返回值是函数累计处理的结果。
一股脑介绍完之后,估计不少同学都是比较懵的。其实这个方法并不难理解的,正如它名字所示,抓住它的核心:聚合。
一般而言,如果需要把数组转换成其他元素,如字符串、数字、对象甚至是一个新数组的时候,若其他数组方法不太适用时,就可以考虑 reduce
方法,不熟悉这个方法的同学,尽管抛开上面的语法, 记住方法的核心是聚合即可。
下文的例子都用到以下数组,假设通过接口获取到如下的数据体:
[{
id: 1,
type: 'A',
total: 3
}, {
id: 2,
type: 'B',
total: 5
}, {
id: 3,
type: 'E',
total: 7
},...]
数据体是按照 id
的升序进行排列,total
与 type
不定~
聚合为数字
根据上述数据体,我们一起来做第一个小需求,统计 total
的总和。如果不用 reduce
,其实也不难:
function sum(arr) {
let sum = 0;
for (let i = 0, len = arr.length; i < len; i++) {
const { total } = arr[i];
sum += total;
}
return sum;
}
这个函数可以完成上述需求,但我们精确地维护了数组索引,再精确地处理整个运算过程,是典型的命令式编程。上文提及,只要涉及将数组转换为另外的数据体,就可以使用 reduce
,它可以这样写:
arr.reduce((sum, { total }) => {
return sum + total;
}, 0)
这样就完成了~sum
是此前累加的结果,它的初始值为 0。每次将此前的累计值加上当前项的 total
为此次回调函数的返回值,作为下次执行时 sum
的实参使用。看起来比较绕,可以参考下面的表格:
轮次 | sum |
total |
返回值 |
---|---|---|---|
1 | 0(初始值) | 3 | 3 |
2 | 3 | 5 | 8 |
3 | 8 | 7 | 15 |
... | ... | ... | ... |
如此是不是清晰了很多?前一次的返回值就是后一次 sum
的值,如此类推,最后累积出总和,将数组聚合成了数字。
聚合为字符串
下一个需求是将数组的每项转换为固定格式的字符串(如第一项转换为 id:1,type:A;
),每项直接以分号作为分隔。一般来说,数组转为字符串,join
方法是不错的选择,但并不适用于需要精确控制或数组的项比较复杂的情况。在本例中,join
方法是达不到我们想要的效果的。
使用 for
循环当然可以解决问题,但 reduce
也许是更好的选择,代码如下:
arr.reduce((str, { id, type }) => {
return str + `id:${id},type:${type};`;
}, '')
有了聚合为数字的例子,这次你能在脑海中模拟出执行的过程么?以下也是前三项的执行过程:
轮次 | str |
id |
type |
返回值 |
---|---|---|---|---|
1 | ''(初始值) | 1 | 'A' | 'id:1,type:A;' |
2 | 'id:1,type:A;' | 2 | 'B' | 'id:1,type:A;id:2,type:B;' |
3 | 'id:1,type:A;id:2,type:B;' | 3 | 'E' | 'id:1,type:A;id:2,type:B;id:3,type:E;' |
... | ... | ... | ... | ... |
聚合为对象
有了前面的一点基础,可以做复杂一点的聚合了。上面的数据体是比较典型的后端接口返回结果,但对于前端来说,转换成 key
value
的对象形式,更利于进行之后的操作。那我们就以转换为 key
是 id
,value
是其他属性的对象作为目标吧!
function changeToObj(arr) {
const res = {};
arr.forEach(({ id, type, total }) => {
res[id] = {
type,
total
};
})
return res;
}
如上所示,这个函数可以很好地完成我们的目标。但略显啰嗦,记住:只要目标是将数组聚合为唯一的元素时,都可以考虑使用 reduce
。这个例子恰好符合:
arr.reduce((res, { id, type, total }) => {
res[id] = {
type,
total
};
return res;
}, {})
res
是最后返回的对象,通过遍历数组,不断往里面添加新的属性与值,最后达到聚合成对象的目的,代码还是相当简洁有力的。
最后,对于不熟悉这个方法的同学,不妨练习一下,将数据体转换为一个字符串数组,数组每一项为原数组 type
的值。
注意事项
-
提供初始值是个好习惯。如果想对数组中的每一项进行一下数据处理后再返回,需保证每一次都是从第0项开始调用,所以此时务必提供
reduce
函数的第二个参数-初始值。否则会从第1项开始调用,第0项反应在callback的第一个参数中
- callback函数中return必不可少。否则无论有没有提供初始值,callback函数的第一个参数永远是
undefined
二、异常处理 & polyfill
异常情况
- 空数组
let a = [];
a.reduce(() => {}, 1); // 1
a.reduce(() => {}); // Uncaught TypeError: Reduce of empty array with no initial value at Array.reduce
这也更加印证了上面提到的那点,提供初始值是个好习惯。
Polyfill
// Production steps of ECMA-262, Edition 5, 15.4.4.21
// Reference: http://es5.github.io/#x15.4.4.21
// https://tc39.github.io/ecma262/#sec-array.prototype.reduce
if (!Array.prototype.reduce) {
Object.defineProperty(Array.prototype, 'reduce', {
value: function(callback /*, initialValue*/) {
if (this === null) {
throw new TypeError( 'Array.prototype.reduce ' +
'called on null or undefined' );
}
if (typeof callback !== 'function') {
throw new TypeError( callback +
' is not a function');
}
// 1. Let O be ? ToObject(this value).
var o = Object(this);
// 2. Let len be ? ToLength(? Get(O, "length")).
var len = o.length >>> 0;
// Steps 3, 4, 5, 6, 7
var k = 0;
var value;
if (arguments.length >= 2) {
value = arguments[1];
} else {
while (k < len && !(k in o)) {
k++;
}
// 3. If len is 0 and initialValue is not present,
// throw a TypeError exception.
if (k >= len) {
throw new TypeError( 'Reduce of empty array ' +
'with no initial value' );
}
value = o[k++];
}
// 8. Repeat, while k < len
while (k < len) {
// a. Let Pk be ! ToString(k).
// b. Let kPresent be ? HasProperty(O, Pk).
// c. If kPresent is true, then
// i. Let kValue be ? Get(O, Pk).
// ii. Let accumulator be ? Call(
// callbackfn, undefined,
// « accumulator, kValue, k, O »).
if (k in o) {
value = callback(value, o[k], k, o);
}
// d. Increase k by 1.
k++;
}
// 9. Return accumulator.
return value;
}
});
}
其中 第三步while循环,是为了排除空值
let arr = Array(2);
console.log(0 in arr); // false
console.log(1 in arr); // false
console.log(arr.length); // 2
第八步while循环中调用callback,同样是避免循环空值
if (k in o) {
value = callback(value, o[k], k, o);
}
空值被跳过
小结
以上就是本文的全部内容。原则上说,只要是将数组聚合为唯一的元素时,都可以使用它。同时,它在函数式编程中有一席之地,也是声明式编程的典型例子。这也意味着它不容易掌握,如果熟悉 reduce
方法,写出来的代码可读性强,十分优雅。但在不熟悉的同学眼里,这就是不折不扣的天书了。如何更好地使用 reduce
,避免写出难以维护的代码,值得每一位同学思考。