Array.prototype.reduce()

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

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

下面的例子能够帮助你理解 reduce() 的用处——计算数组所有元素的总和:

Try it

reducer 逐个遍历数组元素,每一步都将当前元素的值与上一步的计算结果相加(上一步的计算结果是当前元素之前所有元素的总和)——直到没有更多的元素被相加。

语法

// Arrow function
reduce((previousValue, currentValue) => { /* ... */ } )
reduce((previousValue, currentValue, currentIndex) => { /* ... */ } )
reduce((previousValue, currentValue, currentIndex, array) => { /* ... */ } )
reduce((previousValue, currentValue, currentIndex, array) => { /* ... */ }, initialValue)

// Callback function
reduce(callbackFn)
reduce(callbackFn, initialValue)

// Inline callback function
reduce(function(previousValue, currentValue) { /* ... */ })
reduce(function(previousValue, currentValue, currentIndex) { /* ... */ })
reduce(function(previousValue, currentValue, currentIndex, array) { /* ... */ })
reduce(function(previousValue, currentValue, currentIndex, array) { /* ... */ }, initialValue)

参数

callbackFn

一个 “reducer” 函数,包含四个参数:

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

作为第一次调用 callback 函数时参数 previousValue 的值。若指定了初始值 initialValue,则 currentValue 则将使用数组第一个元素;否则 previousValue 将使用数组第一个元素,而 currentValue 将使用数组第二个元素。

返回值

使用 “reducer” 回调函数遍历整个数组后的结果。

异常

TypeError

数组为空且初始值 initialValue 未提供。

描述

ECMAScript 规范描述了 reduce() 的行为:

callbackfn 应是一个接受四个参数的函数,reduce 对于数组中第一个元素之后的每一个元素,按升序各调用一次回调函数。

callbackfn 被调用时会传入四个参数:

  • previousValue(前一次调用 callbackfn 得到的返回值)
  • currentValue(数组中正在处理的元素)
  • currentIndex(数组中正在处理的元素的索引)
  • 被遍历的对象

回调函数第一次执行时,previousValuecurrentValue 的取值有两种情况:

  • 如果调用 reduce() 时提供了 initialValuepreviousValue 取值则为 initialValuecurrentValue 则取数组中的第一个值。
  • 如果没有提供 initialValue,那么 previousValue 取数组中的第一个值,currentValue 取数组中的第二个值。

如果数组为空且未指定初始值 initialValue,则会抛出 TypeError

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

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

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

提供初始值 initialValue 通常更安全,正如下面的例子,如果没有提供 initialValue,则 reduce 方法会因数组长度的不同(大于 1、等于 1、等于 0)而有不同的表现:

const getMax = (a, b) => Math.max(a, b);

// callback is invoked for each element in the array starting at index 0
[1, 100].reduce(getMax, 50); // 100
[    50].reduce(getMax, 10); // 50

// callback is invoked once for element at index 1
[1, 100].reduce(getMax);     // 100

// callback is not invoked
[    50].reduce(getMax);     // 50
[      ].reduce(getMax, 1);  // 1

[      ].reduce(getMax);     // TypeError

无初始值时 reduce() 如何运行

假如运行以下无初始值的 reduce() 代码:

const array = [15, 16, 17, 18, 19];

function reducer(previous, current, index, array) {
  const returns = previous + current;
  console.log(`previous: ${previous}, current: ${current}, index: ${index}, returns: ${returns}`);
  return returns;
}

array.reduce(reducer);

callback 被调用四次,每次调用的参数和返回值如下表:

callback iteration previousValue currentValue currentIndex array return value
first call 15 16 1 [15, 16, 17, 18, 19] 31
second call 31 17 2 [15, 16, 17, 18, 19] 48
third call 48 18 3 [15, 16, 17, 18, 19] 66
fourth call 66 19 4 [15, 16, 17, 18, 19] 85

reduce() 返回的值将是最后一次回调返回值(85)。

有初始值时 reduce() 如何运行

在这里,我们以相同的算法 reduce 同一个数组,但提供 10 作为初始值:

[15, 16, 17, 18, 19].reduce( (previousValue, currentValue, currentIndex, array) => previousValue + currentValue, 10 )
callback iteration previousValue currentValue currentIndex array return value
first call 10 15 0 [15, 16, 17, 18, 19] 25
second call 25 16 1 [15, 16, 17, 18, 19] 41
third call 41 17 2 [15, 16, 17, 18, 19] 58
fourth call 58 18 3 [15, 16, 17, 18, 19] 76
fifth call 76 19 4 [15, 16, 17, 18, 19] 95

这种情况下 reduce() 返回的值是 95

示例

求数组所有值的和

let sum = [0, 1, 2, 3].reduce(function (previousValue, currentValue) {
  return previousValue + currentValue
}, 0)
// sum is 6

你也可以写成箭头函数的形式:

let total = [ 0, 1, 2, 3 ].reduce(
  ( previousValue, currentValue ) => previousValue + currentValue,
  0
)

累加对象数组里的值

要累加对象数组中包含的值,必须提供 initialValue,以便各个 item 正确通过你的函数。

let initialValue = 0
let sum = [{x: 1}, {x: 2}, {x: 3}].reduce(function (previousValue, currentValue) {
    return previousValue + currentValue.x
}, initialValue)

console.log(sum) // logs 6

你也可以写成箭头函数的形式:

let initialValue = 0
let sum = [{x: 1}, {x: 2}, {x: 3}].reduce(
    (previousValue, currentValue) => previousValue + currentValue.x
    , initialValue
)

console.log(sum) // logs 6

将二维数组转化为一维

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

你也可以写成箭头函数的形式:

let flattened = [[0, 1], [2, 3], [4, 5]].reduce(
  ( previousValue, currentValue ) => previousValue.concat(currentValue),
  []
)

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

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] = []
    }
    acc[key].push(obj)
    return acc
  }, {})
}

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

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

// 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'
// ]

数组去重

备注: 如果你正在使用一个可以兼容SetArray.from() 的环境,你可以使用let arrayWithNoDuplicates = Array.from(new Set(myArray)) 来获得一个相同元素被移除的数组。

let myArray = ['a', 'b', 'a', 'b', 'c', 'e', 'e', 'c', 'd', 'd', 'd', 'd']
let myArrayWithNoDuplicates = myArray.reduce(function (previousValue, currentValue) {
  if (previousValue.indexOf(currentValue) === -1) {
    previousValue.push(currentValue)
  }
  return previousValue
}, [])

console.log(myArrayWithNoDuplicates)

使用 .reduce() 替换 .filter().map()

使用 Array.filter()Array.map() 会遍历数组两次,而使用具有相同效果的 Array.reduce() 只需要遍历一次,这样做更加高效。(如果你喜欢 for 循环,你可用使用 Array.forEach() 以在一次遍历中实现过滤和映射数组)

const numbers = [-5, 6, 2, 0];

const doubledPositiveNumbers = numbers.reduce((previousValue, currentValue) => {
  if (currentValue > 0) {
    const doubled = currentValue * 2;
    previousValue.push(doubled);
  }
  return previousValue;
}, []);

console.log(doubledPositiveNumbers); // [12, 4]

按顺序运行 Promise

/**
 * Runs promises from array of functions that can return promises
 * in chained manner
 *
 * @param {array} arr - promise arr
 * @return {Object} promise object
 */
function runPromiseInSequence(arr, input) {
  return arr.reduce(
    (promiseChain, currentFunction) => 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)
  })
}

// function 3  - will be wrapped in a resolved promise by .then()
function f3(a) {
 return a * 3
}

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

const promiseArr = [p1, p2, f3, p4]
runPromiseInSequence(promiseArr, 10)
  .then(console.log)   // 1200

使用函数组合实现管道

// Building-blocks to use for composition
const double = x => x + x
const triple = x => 3 * x
const quadruple = x => 4 * x

// Function composition enabling pipe functionality
const pipe = (...functions) => initialValue => functions.reduce(
    (acc, fn) => fn(acc),
    initialValue
)

// Composed functions for multiplication of specific values
const multiply6 = pipe(double, triple)
const multiply9 = pipe(triple, triple)
const multiply16 = pipe(quadruple, quadruple)
const multiply24 = pipe(double, triple, quadruple)

// Usage
multiply6(6)   // 36
multiply9(9)   // 81
multiply16(16) // 256
multiply24(10) // 240

使用 reduce 实现 map

if (!Array.prototype.mapUsingReduce) {
  Array.prototype.mapUsingReduce = function(callback, initialValue) {
    return this.reduce(function(mappedArray, currentValue, currentIndex, array) {
      mappedArray[currentIndex] = callback.call(initialValue, currentValue, currentIndex, array)
      return mappedArray
    }, [])
  }
}

[1, 2, , 3].mapUsingReduce(
  (currentValue, currentIndex, array) => currentValue + currentIndex + array.length
) // [5, 7, , 10]

规范

Specification
ECMAScript Language Specification
# sec-array.prototype.reduce

浏览器兼容性

BCD tables only load in the browser

参见