数组方法reduce的灵活应用

50 阅读6分钟

Array.prototype.reduce 是什么?

Array.prototype.reduce 的用法是:迭代执行回调函数,并将回调函数返回的值传入给下一轮迭代。取最后一个迭代返回的值,作为 reduce 的返回值。

reduce 这个词又该如何理解?reduce 最常见的意思是 “减少”。根据用法,可能更适合翻译为 “折叠”。reduce 方法将每次迭代产生的结果带到下一次迭代中,最后将二维的线(数组)折叠为一维的点。虽然这个返回值可能也是数组,但从返回值这个维度,还是算作点。

继续详细讲解 reduce 的用法。

reduce 接收两个参数。

第一个参数是回调函数 reducer。

reducer 回调函数会接受由 reduce 提供的 4 个参数:

  1. accumulator:累加器,为上一个迭代执行的回调函数的返回值呀
  2. currentValue:当前迭代的数组元素的值
  3. index:当前迭代的数组元素的索引
  4. array:源数组

当前 reducer 返回的值会在下一次迭代中传入到回调函数中。

第二个参数是初始值,是可选的。

如果不提供初始值,reduce 会将首元素作为累加器初始值,并从第二个数组元素开始迭代。

如果提供初始值,reduce 会将其作为累加器初始值,并从首数组元素开始迭代。

需要注意的是,数组为空的情况下,不提供初始值,会报类型错误。 建议尽量提供初始值,以应对空数组的特殊情况。

Uncaught TypeError: Reduce of empty array with no initial value

2.使用reduce方法的好处

reduce方法内部使用了迭代器,可以解决一些递归类的问题,日常开发中可以使用它解决非常复杂的嵌套对象,例如:在处理大量嵌套数据(如 JSON 文件)时,javascript 中很少有基本的数组方法会被重复使用,可以单独使用 reduce 以用更少的代码实现相同的结果。

3.reduce方法的实际应用

3.1.记录数组中元素的次数

场景描述:

需要根据数组当中存在的值,去记录每次值出现的次数 arr=[1,2,3,4,5,1,2,3],那么结果就是[{1:2},{2:2},{3;2},{4:1},{5:1}]

解决思路:

判断查找当前元素是不是在数组中已经存在过了,如果不存在,那么它就是第一次出现,记录一下该元素已经存在一次了,如果存在就在之前记录的次数上加一.

      function totalNum(arr) {
        const newArr = arr.reduce((pre, cur) => {
          if (!pre[cur]) {
            pre[cur] = 1;
          } else {
            pre[cur]++;
          }
          return pre;
        }, {});
        return newArr;
      }
复制代码

3.2.解决对象数组的去重问题

场景描述:

一般数组去重可以用对象唯一键值的方式,根据object和set的属性名不能重复来判断,但是对象数组就没法判断

解决思路

使用空对象进行存储,从数组中取出对象元素跟目标对象元素进行比对,如果没有,就将当前对象元素push进当前的数组中,否则目标对象中有,则返回空

  function  unbalance(arr) {
        let obj = Object.create(null);
        let newArr = arr.reduce((pre, cur) => {
          obj[cur.name] ? "" : (obj[cur.name] = 1 && pre.push(cur));
          return pre;
        }, []);
        return newArr;
      }
复制代码

3.3.计算年龄

场景描述

有一个年龄数组,它代表人们出生的年份,我们只想保留18岁以上的人,并计算出人们的年龄

const years = [1997, 1999, 2001201220172020];
const currentYear = (new Date).getFullYear();
const reducer = (accumulator, year) => {
  const age = currentYear - year;
  if (age < 18) {
    return accumulator;
  }

  accumulator.push({
    year,
    age
  });

  return accumulator;
}

const over18Ages = years.reduce(reducer, []);
复制代码

3.4.树的数据处理

需求分析

在封装树组件的时候,经过后端返回数据,我们需要将数据加工成我们所需要的数据,将扁平化的数据做嵌套处理,如果不使用递归,就可以通过reduce实现

需要处理的数据

  {"code":0,"parent":[{"name":"文件夹1","pid":0,"id":1},{"name":"文件夹2","pid":0,"id":2},
  {"name":"文件夹3","pid":0,"id":3},{"name":"文件夹1-1","pid":1,"id":4},{"name":"文件夹2-
  1","pid":2,"id":5}],"child":[{"name":"文件1","pid":1,"id":1001},{"name":"文件
  2","pid":1,"id":1002},{"name":"文件2-1","pid":2,"id":1003},{"name":"文件2-
  2","pid":2,"id":1004},{"name":"文件1-1","pid":4,"id":1005},{"name":"文件2-1-1","pid":5,"id":1006}]}
复制代码

处理后的数据

[{"name":"文件夹1","pid":0,"id":1,"children":[{"name":"文件夹1-1","pid":1,"id":4,"children":[{"name":"文件1-1","pid":4,"id":1005}]},{"name":"文件1","pid":1,"id":1001},{"name":"文件
2","pid":1,"id":1002}]},{"name":"文件夹2","pid":0,"id":2,"children":[{"name":"文件夹2-
1","pid":2,"id":5,"children":[{"name":"文件2-1-1","pid":5,"id":1006}]},{"name":"文件2-
1","pid":2,"id":1003},{"name":"文件2-2","pid":2,"id":1004}]},{"name":"文件夹3","pid":0,"id":3}]
复制代码
      //1.先扁平化数组
      const allData = [...data.parent, ...data.child];
      //2.做树的映射表,改成以下结构
     //{1:{name:"文件夹1",pid:0,id:1}}
     const treeData = allData.reduce((pre, cur) => {
        pre[cur["id"]] = cur;
        return pre;
      }, {});
      //3.做数据处理
     const newData = allData.reduce((pre, cur) => {
    //从当前项中拿到pid
        let { pid } = cur;
    //根据映射表判断是否有对应的对象
        let parent = treeData[pid];
        if (parent) {
            // debugger
            // console.log( parent.children,' parent.children');
          parent.children
            ? parent.children.push(cur)
            : (parent.children = [cur]);
        } else if (pid ===0) {
          pre.push(cur);
        }
        return pre;
      }, []);
复制代码

3.5 使用函数实现管道

需求分析

有一堆函数,需要将上一函数的返回值作为下个函数的入参进行计算,实现一个管道函数

      const double = x => x + x;
      const triple = x => 3 * x;
      const quadruple = x => 4 * x;

      const pipe = (...functions) =>initialValue =>functions.reduce((acc, fn) => fn(acc), initialValue);

      const multiply6 = pipe(double, triple);
      const multiply9 = pipe(triple, triple);
      const multiply16 = pipe(quadruple, quadruple);
      const multiply24 = pipe(double, triple, quadruple);

     
      multiply6(6); // 36
      multiply9(9); // 81
      multiply16(16); // 256
      multiply24(10); // 240
      
复制代码

3.6功能函数集成

需求分析

比如我需要将一个字符串切割成数组,在计算数组的长度,然后根据数组的长度做一系列计算操作

      const toWords = (string) => string.split(" ");
      const count = (array) => array.length;
      const wpm = (wordCount) => wordCount * 30;
      const speed = (string, func) =>func.reduce((composed, fn) => fn(composed), string);
      speed("wo shi wangsu", [toWords, count, wpm]);
复制代码

3.7 多个请求的先后执行问题

需求分析

多个请求并发操作,b请求需要在a请求得到相应结果之后操作,除了使用async和await外还可以采用erduce+promise的方式

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

      function p1(a) {
        return new Promise((resolve, reject) => {
          setTimeout(() => {
            resolve(a * 5);
          }, 1000);
        });
      }
      function p2(a) {
        return new Promise((resolve, reject) => {
          setTimeout(() => {
            resolve(a * 2);
          }, 1000);
        });
      }
      function f3(a) {
        return a * 3;
      }

      function p4(a) {
        return new Promise((resolve, reject) => {
          resolve(a * 4);
        });
      }

      const promiseArr = [p1, p2, f3, p4];
      runPromiseInSequence(promiseArr, 10).then(res=>{
        console.log(res);
      });
复制代码

3.8使用reduce迭代器解决递归问题

需求分析

递归函数经常被使用,因为它们的语义定义,,如果可以将递归函数转换为迭代方法,就可以使用reducereduce如果做得好,在启用声明性定义的同时,就不会有可能填满函数堆栈的问题。

const factorial = (number) =>
  number === 0 ? 1 : number * factorial(number - 1);
const factorial = (number) =>
  Array(number)
    .fill(number)
    .reduce((acc, elem, i) => acc * (elem - i));