js reduce方法浅析

242 阅读8分钟

前言

很多年之前, 我刚入行的时候, 听说过这个叫reduce的方法, 但是由于刚入行, 基础薄弱, 经验不足, 对它的了解不够, 因此也不太会使用, 而直到我2017年的时候接触到了react和它的状态管理库redux之后, 在学习react的时候, 在redux的经典例子: todomvc正式看到了用它编写的业务代码: https://github.com/reduxjs/redux/blob/master/examples/todomvc/src/reducers/todos.js:

//...
case ADD_TODO:
  return [
    ...state,
    {
      id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
      completed: false,
      text: action.text
    }
  ]
//...

使用了reduce也就是这一句:

state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1

示例代码表示接收了一个ADD_TODOaction, 然后处理返回了一个新的state, 从字面意思可以看出这是一个新增的action, reduce用在这里是返回一个最大的id并且做了一个+1的操作, 比如我们现在的state是:

[
  {
      "text": "Use Redux",
      "completed": false,
      "id": 0
  },
  {
      "id": 1,
      "completed": false,
      "text": "吃饭"
  },
  {
      "id": 2,
      "completed": false,
      "text": "睡觉"
  }
]

此时我们发送了一个ADD_TODO action:

{
  "type": "ADD_TODO",
  "text": "打游戏"
}

此时得到的state就是:

[
  {
      "text": "Use Redux",
      "completed": false,
      "id": 0
  },
  {
      "id": 1,
      "completed": false,
      "text": "吃饭"
  },
  {
      "id": 2,
      "completed": false,
      "text": "睡觉"
  },
  {
      "id": 3,
      "completed": false,
      "text": "打游戏"
  }
]

可以看到新的state比旧的state多了一个:

{
  "id": 3,
  "completed": false,
  "text": "打游戏"
}

这里的id就是todos.js中的reduce语句的功劳了, 而它具体是怎么做到的呢? 接下来就让我们一起来看看吧

语法

首先, 我们来看看reduce的作用, 看看它会对数组进行什么样的操作: reduce方法会依次对数组中的每一个元素都执行我们传入的回调函数, 并且每次执行都会把上一次的返回值作为第一个参数传入到回调函数中, 最终返回一个值

单了解作用还不够, 最重要的还有它的语法:

Array.prototype.reduce(
  callback(accumulator, currentValue[, index, array]),
  initialValue
);
  1. callback: 我们传入reduce方法中的回调函数
    • accumulator: 上一次调用callback时的返回值

    初次执行: 如果指定了initialValue, 那么它的值就是initialValue, 否则就是Array[0] 后续执行: 它的值为返回值, 如果没有显式地返回, 那么它的值则为undefined

    • currentValue: 当前正在处理的元素. 如果指定了initialValue, 那么它的值是Array[0], 否则它的值是Array[1]
    • index: 当前正在处理的元素的索引. 如果指定了initialValue, 那么它的值是0, 否则它的值为1, 也就是它的值恒为currentValue的索引
    • array: 调用reduce方法的数组
  2. initialValue: 初次调用callbackaccumulator的值

    指定了这个值: accumulator的值为initialValue, currentValue的值为Array[0] 未指定这个值: accumulator的值为Array[0], currentValue的值为Array[1]

reduce方法返回最后一次调用callbackcallback中的返回值, 没有显式的返回则值为undefined

示例

接下来, 我们回过头来看看一开始的redux中的那段代码是如何运行的

示例中的完整业务代码如下:

case ADD_TODO:
  return [
    ...state,
    {
      id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
      completed: false,
      text: action.text
    }
  ]

但如果就直接看reudx todomvc的示例代码的话势必会增加这篇文章的复杂度了, 毕竟还需要对redux有了解, 这不是本文的初衷, 这里我们就只看使用了reduce方法的部分, 这部分代码是这样的:

state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1

我们把它重新写一下, 使之更易读, 同时也更易理解:

state.reduce(
  (maxId, todo) => Math.max(todo.id, maxId),
  -1
) + 1;

这么写之后对照reduce的语法我就可以知道:

callback: (maxId, todo) => Math.max(todo.id, maxId)

initialValue: -1

accumulator: maxId, 字面意思是最大id, 当然了, 指定了initialValue的时候, 初次调用, 它的值为initialValue, 也就是-1

currentValue: todo, state中的每一个元素, 在这个例子中被称作待做事项, 也就是todo, 以及指定了initialValue, 那么初次调用的时候, 它的值为Array[0], 也就是state[0]

最后的+1意思是把reduce的返回值做了一个+1的操作

这里用一个更通俗易懂的示例还原redux todomvcreduce方法的使用:

let array = [1,2,3];

const handleAdd = arr => {
  const res = arr.reduce(
    (maxId, todo) => Math.max(maxId, todo),
    -1
  ) + 1;

  return [
    ...arr,
    res
  ];
};

每次调用我们的handleAdd函数都会给数组中新增一个元素, 且新增的这个元素比之前的元素中的最大值都大1

那么我们一步一步来看, 当我们执行handleAdd(array);调用handleAdd函数的时候reduce方法具体是怎么执行的, arraylength的值为3, 那么我们的reduce方法会调用3次:

第一次调用:

let array = [1,2,3];

const handleAdd = arr => {
  // arr: [1,2,3]
  const res = arr.reduce(
    (accumulator, currentValue) => Math.max(accumulator, currentValue),
    //accumulator: -1
    //currentValue: array[0] 1
    //回调函数返回值: Math.max(-1, 1), 结果为1
    -1
  ) + 1;

  array = [
    ...arr,
    res
  ];
};

由于指定了initialValue那么我们的accumulator的值在第一次执行的时候就是initialValue, 也就是-1

同时也由于指定了initialValue, 在第一次调用时currentValue的值为调用数组索引为0的值, 也就是1

Math.max方法比较两个值取最大的那个值作为返回结果, Math.max(-1, 1)的返回结果为1, 也就是callback的返回值为1

同时这里需要注意的是, reduce方法是同步的方法, 没有执行完是不会执行之后的代码的, 这里只是第一次调用reduce方法, 所以并不会有返回值(返回值是最后一次reduce方法的callback的返回值), 后面的语句也不会执行

第二次调用:

let array = [1,2,3];

const handleAdd = arr => {
  const res = arr.reduce(
    (accumulator, currentValue) => Math.max(accumulator, currentValue),
    //accumulator: 1
    //currentValue: array[1] 2
    //回调函数返回值: Math.max(1, 2), 结果为2
    -1
  ) + 1;

  array = [
    ...arr,
    res
  ];
};

currentValue的值好理解, 依次是array的每一个值, 依次是1 2 3, 关键是累加器也就是accumulator的值, 它每次都是上一次callback的返回值, 而我们第一次调用时候的callback的返回值是1, 因此这一次执行的时候accumulator的值为1

Math.max(1, 2)的结果为2, 也就是callback的返回值为2, 也就是下一次accumulator的值为2

第三次调用:

let array = [1,2,3];

const handleAdd = arr => {
  // arr: [1,2,3]
  const res = arr.reduce(
    (accumulator, currentValue) => Math.max(accumulator, currentValue),
    //accumulator: 2
    //currentValue: array[2] 3
    //回调函数返回值: Math.max(2, 3), 结果为3
    //reduce方法调用结束: reduce的返回值为3
    -1
  ) + 1;
  //执行reduce方法的后续代码: 3+1, res的值为4

  array = [
    ...arr, //[1,2,3]
    res //4
  ]; //[1,2,3,4]
};

由于我们知道accumulator的值为上一次callback的返回值, 上一次这个返回值是2, 所以这一次accumulator的值就为2

Math.max(2, 3), 结果为3

此时reduce调用结束, 返回3, 3+1结果为4, 也就是res的值为4

也可以通过这样的方式打印每个参数:

let array = [1,2,3];

const handleAdd = arr => {
  const res = arr.reduce(
    (accumulator, currentValue) => {
      console.log('accumulator', accumulator);
      console.log('currentValue', currentValue);
      return Math.max(accumulator, currentValue)
    },
    -1
  ) + 1;
  console.log('res', res);

  array = [
    ...arr,
    res
  ];

  console.log('array', array);
};

handleAdd(array);

至此, reduce的一个示例我们就看完了, 接下来我们再看看不在callback中显式返回值的情况

回调函数中不显式返回值

还是以上面的代码为例, 唯一不同的是我们把return语句注释掉:

let array = [1,2,3];

const handleAdd = arr => {
  const res = arr.reduce(
    (accumulator, currentValue) => {
      console.log('accumulator', accumulator);
      console.log('currentValue', currentValue);
      // return Math.max(accumulator, currentValue)
    },
    -1
  ) + 1;
  console.log('res', res);

  array = [
    ...arr,
    res
  ];

  console.log('array', array);
};

handleAdd(array);

初次执行:

accumulator: -1
currentValue: 1

第二次执行:

accumulator: undefined
currentValue: 2

第三次执行:

accumulator: undefined
currentValue: 3
reduce方法返回值: undefined
res: undefined+1, NaN
array: [1,2,3,NaN]

这里我们可以看到, 当reduce方法的回调函数中不显式地将值返回出去的话, 它的结果将是undefined, 同时由于accumulator的值是上一次回调函数的返回值, 因此从第二次调用开始, accumulator都始终为undefined

其他常见应用

接下来和大家分享几个reduce的常见应用

求和

这是reduce最简单, 也最常见的应用:

const array = [1,2,3];

const sum = array.reduce(
  (acc, cur) => acc + cur,
  0
);

这里不指定初始值也是可以的

求积

const array = [1,2,3];

const product = array.reduce(
  (acc, cur) => acc * cur
);

数组去重

数组去重在平时的开发过程中也会经常用到, 网上也有很多处理方法, 这里给大家介绍一个使用reduce的去重的方法:

const array = [1,2,3,2,4,1,5];

const unique = array.reduce(
  (acc, cur) => acc.includes(cur) ? acc : [...acc, cur],
  []
); //[1,2,3,4,5]

这里需要注意的是, 初始值要设为[], 回调函数主要是做判断处理, 将不重复的值存入数组中, 既然要存入数组中, 那么就需要注意返回值必须是个数组, 这里使用数组字面量展开运算符能完美处理存入数组和返回输入的操作

总结

reduce方法除了可以像for forEach map等方法一样遍历数组之外, 最强大的地方还是在于积累: 它将积累的结果应用到了数组的每一个元素中去, 每一次遍历积累的结果都可以用于下一次的遍历, 这是它的强大之处, 同时也是和for forEach map等方法的不同之处

以上就是这篇文章的全部内容了, 如果你觉得这篇文章写得还不错, 别忘了给我点个赞, 如果你觉得对你有帮助, 可以点个收藏, 以备不时之需

参考文献:

  1. Array.prototype.reduce()
  2. Reduce 和 Transduce 的含义