Array.prototype.reduce()

reduce() メソッドは、配列のそれぞれの要素に対してユーザーが提供した「縮小」コールバック関数を呼び出します。その際、直前の要素における計算結果の返値を渡します。配列のすべての要素に対して縮小関数を実行した結果が単一の値が最終結果になります。

reduce() で一番わかりやすいのは、配列のすべての要素の和を返す場合でしょう。

縮小関数は配列を要素ごとに走査し、それぞれの段階で、前の段階の結果に現在の配列の値を加えていきます (この結果は、それ以前のすべての段階を合算したものです)。

次のインタラクティブサンプルで紹介します。

構文

// アロー関数
reduce((previousValue, currentValue) => { ... } )
reduce((previousValue, currentValue, currentIndex) => { ... } )
reduce((previousValue, currentValue, currentIndex, array) => { ... } )
reduce((previousValue, currentValue, currentIndex, array) => { ... }, initialValue)

// コールバック関数
reduce(callbackFn)
reduce(callbackFn, initialValue)

// インラインコールバック関数
reduce(function callbackFn(previousValue, currentValue) { ... })
reduce(function callbackFn(previousValue, currentValue, currentIndex) { ... })
reduce(function callbackFn(previousValue, currentValue, currentIndex, array){ ... })
reduce(function callbackFn(previousValue, currentValue, currentIndex, array) { ... }, initialValue)

引数

callbackFn

4 つの引数を取る「縮小」関数です。

  • previousValue (前回の callbackfn の呼び出し結果の値)
  • currentValue (現在の要素の値)
  • currentIndex (現在の位置) 省略可
  • array (走査する配列) 省略可
initialValue 省略可

コールバックが初めて呼び出されたときの previousValue の初期値です。 initialValue が指定された場合は、 currentValue も配列の最初の値に初期化されます。 initialValue が指定されなかった場合、 previousValue は配列の最初の値で初期化され、 currentValue は配列の 2 番目の値で初期化されます。

返値

配列全体にわたって「縮小」コールバック関数を実行した結果の値です。

例外

TypeError: 配列に要素がなく、かつ initialValue が提供されなかった場合に発生します。

解説

ECMAScript の仕様書は、 reduce() の動作を次のように記述しています。

callbackfn は、4 つの引数を取る関数でなければなりません。 reduce は、配列の最初の要素の後の各要素に対して、昇順にコールバックを関数として呼び出します。

callbackfn は次の 4 つの引数で呼び出されます。

  • the previousValue (前回の callbackfn の呼び出し結果の値)
  • the currentValue (現在の要素の値)
  • the currentIndex
  • 走査中のオブジェクト

そのコールバックが最初に呼び出されるとき、 previousValuecurrentValue は以下の 2 通りのうちのどちらかになります。

  • initialValuereduce の呼び出しによって与えられた場合、 previousValue は to initialValue と同じになり、 currentValue は配列の最初の値と等しくなります。
  • initialValue が与えられていない場合は、 previousValue は配列の最初の値と同じになり、 currentValue は 2 番目の値と同じになります。

配列に要素がなく、かつ initialValue が与えられていない場合は TypeError が発生します。

reduce は、呼び出されたオブジェクトを直接は変更しませんが、 callbackfn の呼び出しによってオブジェクトが変更される可能性はあります。

reduce で処理される要素の範囲は、 callbackfn が最初に呼び出される前に設定されます。 reduce の呼び出しが始まった後に配列に追加された要素は、 callbackfn が処理することはありません。配列の既存の要素が変更された場合、 callbackfn には reduce がその値を処理する時点の値が渡されます。 reduce が呼び出された後、処理されるまでに削除された要素は処理されません。

配列が (位置に関わらず) 1 つの要素しか持たず、initialValue が指定されなかった場合、または initialValue が指定されていても配列が空だった場合、 callbackfn実行されずに要素が返却されます。

initialValue が提供され、配列が空でない場合、 reduce メソッドは常に 0 の位置コールバック関数を呼び出し始めます。

initialValue が提供されなかった場合、 reduce メソッドは、次の例に示すように、長さが 1 より大きい配列、長さが 1 の配列、長さが 0 の配列に対して異なる動作をします。

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

// コールバックは 0 の位置から配列内の全要素に対して呼び出される
[1, 100].reduce(getMax, 50); // 100
[    50].reduce(getMax, 10); // 50

// コールバックは 1 の位置に対して 1 度だけ呼び出される
[1, 100].reduce(getMax);     // 100

// コールバックは呼び出されない
[    50].reduce(getMax);     // 50
[      ].reduce(getMax, 1);  // 1

[      ].reduce(getMax);     // TypeError

reduce() の動作

reduce() を以下のように使うことを想像してください。

[0, 1, 2, 3, 4].reduce(function(previousValue, currentValue, currentIndex, array) {
  return previousValue + currentValue
})

コールバック関数は 4 回呼び出され、各回の引数の内容は以下のようになります。

callback の反復処理 previousValue currentValue currentIndex array 返値
初回の呼出し 0 1 1 [0, 1, 2, 3, 4] 1
2 回目の呼出し 1 2 2 [0, 1, 2, 3, 4] 3
3 回目の呼出し 3 3 3 [0, 1, 2, 3, 4] 6
4 回目の呼出し 6 4 4 [0, 1, 2, 3, 4] 10

reduce() の返値は、コールバック呼び出しの最後の返値である (10) となるでしょう。

通常の関数の代わりにアロー関数を指定することができます。下記のコードは上記のコードと同じ結果を返します。

[0, 1, 2, 3, 4].reduce( (previousValue, currentValue, currentIndex, array) => previousValue + currentValue )

initialValuereduce() の 2 つ目の引数に渡した場合は、結果は次のようになります。

[0, 1, 2, 3, 4].reduce((previousValue, currentValue, currentIndex, array) => {
    return previousValue + currentValue
}, 10)
callback の反復処理 previousValue currentValue currentIndex array 返値
初回の呼出し 10 0 0 [0, 1, 2, 3, 4] 10
2 回目の呼出し 10 1 1 [0, 1, 2, 3, 4] 11
3 回目の呼出し 11 2 2 [0, 1, 2, 3, 4] 13
4 回目の呼出し 13 3 3 [0, 1, 2, 3, 4] 16
5 回目の呼出し 16 4 4 [0, 1, 2, 3, 4] 20

この場合の reduce() の返値は 20 となります。

配列内の値の合計値を出す

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 を指定する必要があります

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 }

プロパティによってオブジェクトをグループ化

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

配列内の重複要素を除去する

Note: SetArray.from() に対応している環境を使っている場合は、let orderedArray = 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)

.filter().map() を .reduce() で置き換える

Array.filter() を使用した後で Array.map() を使用すると配列を二度走査しますが、Array.reduce() では同じ効果を一度の操作で実現することができ、もっと効率的です。(for ループが好きなのであれば、Array.forEach() で一度の操作で filter と map を行うことができます)。

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) => input => functions.reduce(
    (acc, fn) => fn(acc),
    input
)

// 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[index] = 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 (ECMAScript)
# sec-array.prototype.reduce

ブラウザーの互換性

BCD tables only load in the browser

関連情報