数组原生reduce方法不是很熟悉,只停留在可以进行“累加求和”的理解上,平常编码中也很少使用到该方法,往往被大家搁置在角落里,但实际上该方法属于高阶函数中的高阶,比起常用的forEach/map/filter等函数具有更强的灵活性与自由度,编码上也更精炼简洁。
本文主要介绍几种reduce可以处理的应用场景,抛砖引玉,期待大家在日常开发中有更多的灵感发挥!
基本使用
语法
arr.reduce(callback(accumulator, currentValue, index, array), [initialValue])
// 对数组求和
[1, 2, 3, 4, 5].reduce((prev, curr) => prev + curr, 0);
复制代码
可以看到reduce方法接收2个参数:
第一个参数callback为Function类型,用于迭代数组每一项,该函数接收4个参数:
accumulator: 初始值(或者上一次回调函数的返回值)
currentValue: 当前元素值
Index?: 当前索引
array?: 调用 reduce 的数组
第二个参数initialValue为可选参数,类型为any,作为迭代函数的初始值。
注意:如果不写initialValue参数,则默认将数组第一项元素作为初始值,并从第二项开始迭代。
兼容性
由于reduce为ES5规范的产物,因此大部分浏览器天然支持,除了IE6~8等老古董外,可以放心使用。
特别注意
根据使用经验来看,很容易将reduce的用法与forEach/map混淆,特别是在第一个参数的返回值上,
在callback函数内必须显性有返回值,否则可能会导致程序存在隐性bug!
在callback函数内必须显性有返回值,否则可能会导致程序存在隐性bug!
在callback函数内必须显性有返回值,否则可能会导致程序存在隐性bug!
重要事情说三遍!
应用场景
从上述用法上可以知道,第二个参数初始值可以为任意类型值,列举几种类型有:数字、字符串、数组、普通对象、函数、Promise对象等,下面分别对这几种初始值进行应用。
数值累加
初始值为数字时,可以进行数字的运算:
const list = [
{
subject: 'math',
score: 80
},
{
subject: 'chinese',
score: 90
},
{
subject: 'english',
score: 100
}
];
const sum = list.reduce((prev, cur) => cur.score + prev, 30);
console.log(sum); // 300
复制代码
字符串拼接
初始值为字符串时,可以进行字符串拼接:
const list = ['zhang', 'san', 'feng'];
const name = list.reduce((prev, cur) => prev + cur, 'my name is: ');
console.log(name); // my name is: zhangsanfeng
复制代码
数组扁平化
初始值为数组时就比较灵活了,与数字和字符串最大的区别在于数组的引用可以一直保持不变的迭代下去,直到最后作为reduce的返回值返回。这里的数组可以传空数组,也可以是带有默认参数的数组,甚至是多维数组进行组装:
const arr = [[0, 1], [2, 3], [4, [5, 6, 7]]];
function flattern(arr) {
return arr.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur) ? flattern(cur) : cur)
}, []);
}
复制代码
实现map方法
这是一个经典面试,原理同上,此外还可以实现filter/sort等方法。
Array.prototype.myMap = function(callback, thisArg) {
return this.reduce((memo, item, index, array) => {
memo[index] = callback.call(thisArg, item, index, array);
return memo;
}, []);
}
Array.prototype.mySort = function() {
return this.reduce((sorted, item) => {
let index = 0;
while(index < sorted.length && item > sorted[index]) index++;
sorted.splice(index, 0, item);
return sorted;
}, []);
}
复制代码
那么,既然通过reduce能实现大部分的功能,为什么浏览器还要提供那么多的原生方法?下面简单的比对了一下原生map与基于reduce实现的myMap处理100万数据量的耗时情况,脚本如下:
var arr = [...new Array(1000000)].map((_, index) => index);
// origin map
console.time('map');
arr.map(item => item * 2);
console.timeEnd('map');
// myMap
console.time('myMap');
arr.myMap(item => item * 2);
console.timeEnd('myMap');
复制代码
在笔者本人mac pro谷歌浏览器上运行结果显示,原生map的耗时在18ms左右,myMap耗时在34ms左右,显然,原生方法比reduce实现的耗时少了将近一半,因此浏览器层面针对map api增加了优化。
数组分类
传入普通对象最大特点也是引用不变原则,可以在一次迭代中完成对年龄与性别的分类:
const students = [
{ name: 'Alex', age: 21, sex: 'male' },
{ name: 'Max', age: 20, sex: 'male' },
{ name: 'Jane', age: 20, sex: 'female' }
];
const result = students.reduce((memo, student) => {
(memo.age[student.age] = memo.age[student.age] || []).push(student);
(memo.sex[student.sex] = memo.sex[student.sex] || []).push(student);
return memo; // 容易漏写
}, { age: {}, sex: {} });
console.log(result);
// 输出
{
"age": {
"20": [
{
"name": "Max",
"age": 20,
"sex": "male"
},
{
"name": "Jane",
"age": 20,
"sex": "female"
}
],
"21": [
{
"name": "Alex",
"age": 21,
"sex": "male"
}
]
},
"sex": {
"male": [
{
"name": "Alex",
"age": 21,
"sex": "male"
},
{
"name": "Max",
"age": 20,
"sex": "male"
}
],
"female": [
{
"name": "Jane",
"age": 20,
"sex": "female"
}
]
}
}
复制代码
实现Promise.all
初始值为Promise.resolve(),迭代之后可以形成then微任务的链式调用:
function all (promises, results = []) {
return promises
.reduce(
(memo, p) => {
return memo.then(() => p).then((r) => {
console.log(r);
results.push(r);
})
},
Promise.resolve(),
)
.then(() => results);
}
console.time('time');
all([
new Promise((r) => setTimeout(r, 2000, 'a')),
new Promise((r) => setTimeout(r, 3000, 'b')),
new Promise((r) => setTimeout(r, 1000, 'c')),
]).then((ret) => console.timeEnd('time'));
复制代码
以上Promise.all可以看着是并行调用,如果想要实现串行调用,想想可以怎么改动?
function seriesAll (promises, results = []) {
return promises
.reduce(
(memo, p) => {
return memo.then(p).then((r) => {
console.log(r);
results.push(r);
})
},
Promise.resolve(),
)
.then(() => results);
}
console.time('time2');
seriesAll([
() => new Promise((r) => setTimeout(r, 2000, 'a')),
() => new Promise((r) => setTimeout(r, 3000, 'b')),
() => new Promise((r) => setTimeout(r, 1000, 'c')),
]).then(() => console.timeEnd('time2'));
复制代码
一个原则,reduce函数的执行一定是同步的,而且一定是属于宏任务代码块,剩下的其实就是对then微任务执行时机的理解了,只有当前宏任务中的Promise状态切换为fulfilled后才会进入本轮微任务队列。
compose组合
compose有个专业叫法,叫函数组合,基本实现与下面类似,利用reduce可以返回函数的特性+闭包,实现多个函数链式调用。其中初始值可以传一个空函数,也可以不传,注意调用顺序是从后往前。
redux的中间件洋葱模型也是基于compose方法实现,有兴趣可以看看官方实现
Polyfill
该polyfill来自MDN,但项目中基本用不上了,此处只是特别看下reduce
的基本实现原理,加深对reduce
理解。
// 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;
}
});
}
复制代码
大道至简,可以看到其实reduce的实现也是很简单的,给我们的启示是在设计API的时候不需要设计的很复杂,保留足够的自由度与灵活性给到开发者自己去“添油加醋”,给菜谱不如给原料,尝试与探索或许会是一道更好的菜。