【深度总结】数组 reduce 一看一整天(附实现代码)

2,662 阅读5分钟

【深度总结】 JS数组 reduce 相关的方法,原理,应用,还有 polyfill 实现的 reduce, 其实你可以直接调到最后一章,了解一下 reduce 的实现即可。


💁‍♂️ Array.reduce

  1. 简述:用于迭代累加。
  2. 语法:arr.reduce(callback [, initialValue])
  3. 参数:
    1. callback: 针对每一项执行的函数。
    2. inititalValue: 可选,累加执行的初始值。
    3. callback
      • (acc) 初始值/上一次回调的返回值值
      • (cur) 当前值
      • (idx) 可选,当前索引
      • (src) 可选,源数组
  • 描述: 方法接收一个函数作为累加器,reduce 为数组中的每一个元素依次执行回调函数,不包括数组中被删除或从未被赋值的元素。

💁‍♂️ 基本用法:

const array = [1, 2, 3, 4];
const reducer = (pre, cur) => pre + cur;
// 10 + 1 + 2 + 3 + 4
console.log(array.reduce(reducer, 10));

💁‍♂️ Tips:

  • (1) reducer 函数的返回值分配给累计器,该返回值在数组的每个迭代中被记住,并最后成为最终的单个结果值。
  • (2) 如果没有初始值,pre 将使用数组中的第一个元素。
  • (3) 在没有初始值空数组上调用 reduce 将报错。
  • (4) 再没有初始值仅有一个元素的数组上使用reduce,那么callback 不会 被执行 ,此唯一值将被返回。

💁‍♂️ 问题

下面代码中,Array.reduce(callback) 的callback函数被执行了几次

[8, 1, 2, 3, 4].reduce(function(pre, cur, index, array){
  return pre + cur;
});

分析:

  • 没有初始值的时候,callback 被执行了4次。 第1次执行的时候: pre=8,cur=1,index=1,array是数组。
  • 在有初始值的时候, callback 会被执行 5 次。第1次执行的时候:pre=初始值,cur=8,index=0,array是数组。
  • 初始值是0,也是有初始值,也会被执行 5 次。

💁‍♂️ 应用

📝 1. 数组每一项的和

var total = [ 0, 1, 2, 3 ].reduce( ( acc, cur ) => acc + cur, 0);

📝 2. 累加对象数组中的值

var init = 0;
var sum = [{x: 1}, {x:2}, {x:3}].reduce( (pre, cur) {
    return pre + cur.x;
}, init)
  • 注意: 这里必需使用初始值,以保证每一个回调函数都被执行。

📝 3. 二维数组转换成一维

var array = [[0, 1], [2, 3], [4, 5]];
var flattened = array.reduce( ( pre, cur ) => pre.concat(cur), [] );
  • 注意: 这里的初始值应该为空数组,以保证第一次应用callback的时候 pre = [], pre才可以使用 [].concat 方法进行数组合并。

📝 4. 计算数组中每个元素出现的次数

var names = ['Alice', 'Bob', 'Tiff', 'Bruce', 'Alice'];

var countedNames = names.reduce(function (allNames, name) { 
  if (name in allNames) {
    allNames[name]++;
  }
  else {
    allNames[name] = 1;
  }
  return allNames;
}, {});
  • 注意:这里 allNames 使用了 pre 是所有已经处理过内容的集合 这一特性。
  • 使用: in 运算符判断属性名是否存在: 属性名 in 对象
  • init 设置为空对象。

📝 5. 按属性对object分类

var people = [
  { name: 'Alice', age: 21 },
  { name: 'Max', age: 20 },
  { name: 'Jane', age: 20 }
];

function groupBy(objectArray, property) {
  return objectArray.reduce(function (acc, obj) {
    var key = obj[property];
    if (!acc[key]) {
      acc[key] = [];
    }
    acc[key].push(obj);
    return acc;
  }, {});
}

var groupedPeople = groupBy(people, 'age');
// groupedPeople is:
// { 
//   20: [
//     { name: 'Max', age: 20 }, 
//     { name: 'Jane', age: 20 }
//   ], 
//   21: [{ name: 'Alice', age: 21 }] 
// }

📝 6. 合并对象数组中的某一项

var people = [{
  name: 'Anna',
  books: ['Bible', 'Harry Potter'],
  age: 21
}, {
  name: 'Bob',
  books: ['War and peace', 'Romeo and Juliet'],
  age: 26
}, {
  name: 'Alice',
  books: ['The Lord of the Rings', 'The Shining'],
  age: 18
}];

// 合并所有书的名字
var allbooks = friends.reduce(function(prev, curr) {
  return [...prev, ...curr.books];
}, []);
// allbooks = [
//   'Bible', 'Harry Potter', 'War and peace', 
//   'Romeo and Juliet', 'The Lord of the Rings',
//   'The Shining'
// ]

📝 7. 数组去重

let arr = [1,2,1,2,3,5,4,5,3,4,4,4,4];
let result = arr.sort().reduce((pre, current) => {
    // 原理看解释
    if(pre.length === 0 || pre[pre.length-1] !== current) {
        pre.push(current);
    }
    return pre;
}, []);
console.log(result); //[1,2,3,4,5]
  • 数组是空的时候,直接添加这一项
  • 数组的最后一项(pre[pre.length-1])和当前值(cur)不相等的时候,也要添加,因为现在的数组是排好序的 arr.sort(),pre的最后一项和现在这项不等: 说明这是一个新的数字,需要添加到数组中。

📝 8. 按顺序运行Promise

// arr: Promise数组
// input: 其它参数

function runPromiseInSequence(arr, input) {
  return arr.reduce( function(promiseChain, currentFunction) {
    return promiseChain.then(currentFunction)
  }, Promise.resolve(input) )
}

// promise function 1
function p1(a) {
  return new Promise((resolve, reject) => {
    resolve(a * 5);
  });
}

// promise function 2
function p2(a) {
  return new Promise((resolve, reject) => {
    resolve(a * 2);
  });
}

const promiseArr = [p1, p2];

runPromiseInSequence(promiseArr, 10).then(console.log);   // 100

💁‍♂️ polyfill : 自己实现一个 Array.reduce()

在实现一个简单的 Array.reduce()之前,先回顾一下 Array.reduce()的几个关键点。划重点~

  • 1、 Array.reduce 是Array.prototype 上的原生方法。
  • 2、 Array.reduce 接受两个参数: callback函数 和 init 初始值[可选]
  • 3、 👨‍💻 没有初始值 && 数组是空 => 报错
  • 4、 👨‍💻 没有初始值 && 数组长度1 => 直接返回这一项,不执行callback
  • 5、 👨‍💻 没有初始值 && 长度大于1 => 这是一个正常的没有初始值数组 => 从第二项开始执行。prev = array[0] curr=array[1]
  • 6、 👨‍💻 有初始值 && 长度大于1 => 从第一项开始执行 => prev=init curr = array[0]
  • 7、 👨‍💻 reduce 函数的返回值是 prev。

// ① 定义到 Array.prototype 上。 函数有两个参数
Array.prototype.myReduse = function(callback, init) {
    // init 是可选的, 这里的 this 是调用的那个array。
    init = init || null;
    const array = this;
    
    // ② 定义 prev 之前的结果 和 循环起始点
    let prev, startIndex = 0;
    // ③ 没有初始值 && 数组是空  => 报错
    if(!init && array.length == 0 ) return console.log('空数组,没有初始值');
    // 没有初始值 && 数组长度=1  => 直接返回第一项,callback不执行
    if(!init && array.length == 1 ) return array[0];
    // 没有初始值 && 长度大于1  =>  从第二项开始,prev = 第一项的值
    if(!init && array.length > 1) {
        prev = array[0];
        startIndex = 1;
    } 
    // 有初始值 && 长度大于1  =>  从第一项开始,prev = init 初始值
    if(init && array.length > 1) {
        prev = init;
    } 

    
    for(let index = startIndex; index < array.length; index ++) {   
        // prev 始终是回调函数执行的结果,
        // callback 按照顺序传入值: 前面的结果, 当前项的值, 当前索引,原数组
        prev = callback(prev, array[index], index, array);
    }

    // 返回之前项的结果。
    return prev;
}

这里是 MDN 的 polyfill 方式