JS数组的reduce方法

791 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情

定义:

reduce() 方法对数组中的每个元素按序执行一个由您提供的 reducer 函数,每一次运行 reducer 会将先前元素的计算结果作为参数传入,最后将其结果汇总为单个返回值。

首先我们可以看到,reduce返回的是一个单值,该单值是由每一项计算出来的

第一次执行回调函数时,不存在“上一次的计算结果”。如果需要回调函数从数组索引为 0 的元素开始执行,则需要传递初始值。否则,数组索引为 0 的元素将被作为初始值 initialValue,迭代器将从第二个元素开始执行(索引为 1 而不是 0)。

const array1 = [1, 2, 3, 4];
​
// 0 + 1 + 2 + 3 + 4
const init = 0;
const sum = array1.reduce(
  (pre, cur) => {
      pre + cur
  },
  init
);
​
console.log(sum);  // 10

由上面的例子可知,reduce需要传三个参数,precur和初始值init

  • previousValue:上一次调用 callbackFn 时的返回值。在第一次调用时,若指定了初始值 initialValue,其值则为 initialValue,否则为数组索引为 0 的元素 array[0]
  • currentValue:数组中正在处理的元素。在第一次调用时,若指定了初始值 initialValue,其值则为数组索引为 0 的元素 array[0],否则为 array[1]
  • currentIndex:数组中正在处理的元素的索引。若指定了初始值 initialValue,则起始索引号为 0,否则从索引 1 起始。

那么上面的例子可以拆分为以下几个步骤

  1. 第一次执行的时候,因为传了initpreinitcur为数组第一项1,就是0 + 1,所以第一次执行的结果为1
  2. 第二次执行时,pre为第一次执行的结果1,cur为数组第二项2,就是1 + 2,所以第二次执行的结果为3
  3. 第二次执行时,pre为第二次执行的结果3,cur为数组第三项3,就是3 + 3,所以第三次执行的结果为6
  4. 第四次执行时,pre为第三次执行的结果6,cur为数组第四项4,就是6 + 4,所以第三次执行的结果为10
  5. 第五次执行时,pre为第三次执行的结果10,因为此时数组循环完毕,curundefined,所以循环结束,输出pre的值,所以最后的结果为10

值得注意的是

reduce 不会直接改变调用它的对象,但对象可被调用的 callbackfn 所改变。

遍历的元素范围是在第一次调用 callbackfn 之前确定的。所以即使有元素在调用开始后被追加到数组中,这些元素也不会被 callbackfn 访问。如果数组现有的元素发生了变化,传递给 callbackfn 的值将会是元素被 reduce 访问时的值(即发生变化后的值);在调用 reduce 开始后,尚未被访问的元素若被删除,则其将不会被 reduce 访问。

如果数组仅有一个元素(无论位置如何)并且没有提供初始值 initialValue,或者有提供 initialValue 但是数组为空,那么此唯一值将被返回且 callbackfn 不会被执行。

ok现在我们来做几道简单的实例题

累加对象的数组里面的值

注意这里因为我们传递了初始值,所以第一次previousValue的值为初始值0,这时候如果定义previousValue.x就会报错

let arr = [
    {x: 1}, 
    {x: 2}, 
    {x: 3}
]
​
let initialValue = 0
let sum = arr.reduce(function (previousValue, currentValue) {
    return previousValue + currentValue.x
    //这里
}, initialValue)
​
console.log(sum) // logs 6

将二维数组转化为一维

这里的初始值可以不传,因为每一项都是数组,都可以执行concat方法

let flattened = [[0, 1], [2, 3], [4, 5]].reduce(
  function(previousValue, currentValue) {
    return previousValue.concat(currentValue)
  },
  []
)
// flattened is [0, 1, 2, 3, 4, 5]

计算数组中每一个元素出现的次数

这个方法跟for循环非常像,都是把每个元素存储为对象的键,出现的次数为值,最后返回整个对象

let names = ['Alice', 'Bob', 'Tiff', 'Bruce', 'Alice']
​
let countedNames = names.reduce(function (allNames, name) {
  if (name in allNames) {
    allNames[name]++
  }
  else {
    allNames[name] = 1
  }
  return allNames
}, {})
// countedNames is:
// { 'Alice': 2, 'Bob': 1, 'Tiff': 1, 'Bruce': 1 }

按属性对object分类

这个方法中是将需要分的类作为键,每一个键相同的对象组合为一个数组,作为该键的值

let people = [
  { name: 'Alice', age: 21 },
  { name: 'Max', age: 20 },
  { name: 'Jane', age: 20 }
];
​
function groupBy(objectArray, property) {
  return objectArray.reduce(function (acc, obj) {
    let key = obj[property]
    if (!acc[key]) {
      acc[key] = []
    }
    //这里第一执行的时候结果为 {21:[]}
    acc[key].push(obj)
    //{21:[{ name: 'Alice', age: 21 }]}
    return acc
  }, {})
}
​
let groupedPeople = groupBy(people, 'age')
// groupedPeople is:
// {
//   20: [
//     { name: 'Max', age: 20 },
//     { name: 'Jane', age: 20 }
//   ],
//   21: [{ name: 'Alice', age: 21 }]
// }

使用扩展运算符和 initialValue 绑定包含在对象数组中的数组

扩展运算符...将初始值['Alphabet']和数组中每一个对象的books属性组合成一个大的数组

// friends - an array of objects
// where object field "books" is a list of favorite books
let friends = [{
  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
}]
​
// allbooks - list which will contain all friends' books +
// additional list contained in initialValue
let allbooks = friends.reduce(function(previousValue, currentValue) {
  return [...previousValue, ...currentValue.books]
}, ['Alphabet'])
​
// allbooks = [
//   'Alphabet', 'Bible', 'Harry Potter', 'War and peace',
//   'Romeo and Juliet', 'The Lord of the Rings',
//   'The Shining'
// ]

篇幅有限,下一篇将详细介绍reduce的更多用法!