配列界最強のメソッドです。(?) 万能選手ですがどうか使わないでください。
使い方はちょっとわかりづらいんだけど、こんな↓感じです。
const arr = [11, 22, 33]; const obj = arr.reduce((acc, value, index) => { acc[`number${index}`] = value; return acc; }, {}); console.log(obj); // { number0: 11, number1: 22, number2: 33 }
与えたコールバック関数の次に第2引数がある点に注目。
インターフェイスと使い方
result = arr.reduce(callback[, initialValue])
// callback = (accumulator, currentValue[, currentIndex, array]) => accumulator;
引数
コールバック関数と初期値の2つです。
第2引数の初期値に、例えばここに空のオブジェクトを与えたりします。
第1引数のコールバック関数が最初に受け取る第1引数は、その初期値になります。その値を操作して return
すると、2回目以降のコールバック呼び出しの第1引数にはその return
した値が与えられます。
戻り値
順に繰り返し、コールバック関数が最後に return
した値が、 reduce()
全体の戻り値になります。
初期値をそのまま返してみる例
initial
がコールバック関数の第1引数 acc
として与えられて、毎回それを return
するので以降も同じインスタンスを受け取り、最終的に結果もそれになります。
const arr = [11, 22, 33]; const initial = {}; const result = arr.reduce((acc, _, index) => { console.log(index, acc === initial); return acc; }, initial); console.log('result', result === initial); // 0 true // 1 true // 2 true // result true
初回だけ初期値を受け取ってみる例
return
時に新しいインスタンスを生成してみます。2回目はそれを受け取るので false
、また新しいインスタンスになって3回目も同じく false
です。
const arr = [11, 22, 33]; const initial = {}; const result = arr.reduce((acc, _, index) => { console.log(index, acc === initial); return { ...acc }; }, initial); console.log('result', result === initial); // 0 true // 1 false // 2 false // result false
数値を増やしてみる例
繰り返しながら操作する対象はオブジェクトに限らず、このように数値でも可能です。
const arr = [11, 22, 33]; const result = arr.reduce((acc, _, index) => { console.log(index, acc); return acc + 1; }, 100); console.log('result', result); // 0 100 // 1 101 // 2 102 // result 103
初期値を省略した例
省略すると配列の最初の要素が与えられます。またコールバック関数はその次の要素から呼ばれます。
この例↓だと最初 arr[0]
の 11
がコールバック関数で処理されず、最初から acc
へ与えられます。
const arr = [11, 22, 33]; const result = arr.reduce((acc, value, index) => { console.log(index, acc, value); return value; }); console.log('result', result); // 1 11 22 // 2 22 33 // result 33
例
合計
これが理想的な reduce()
の使い方です。
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; const sum = arr.reduce((acc, value) => acc + value); console.log(sum); // => 55
最大値
Math.max(...arr)
でやれるやつ。
const arr = [94, 39, 63, 87, 52, 3, 10, 95, 5]; const max = arr.reduce((acc, value) => (acc > value ? acc : value)); console.log(max); // => 95
マップ
map()
代わり。
const arr = [11, 22, 33]; const result = arr.reduce((acc, value, index) => { acc[index] = 1000 + value; return acc; }, []); console.log(result); // [ 1011, 1022, 1033 ]
フィルター
filter()
代わり。
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; const result = arr.reduce((acc, value) => { if (value % 3 === 0) { acc.push(value); } return acc; }, []); console.log(result); // [ 3, 6, 9 ]
平坦化
flat()
の代わり。
const arr = [ 11, [22, 33], 44, [55, 66, 77], ]; const result = arr.reduce((acc, value) => acc.concat(value), []); console.log(result); // [ 11, 22, 33, 44, 55, 66, 77 ]
検索
find()
代わり。
const arr = [ { id: '101', name: 'Alice' }, { id: '102', name: 'Bob' }, { id: '103', name: 'Charlie' }, ]; const result = arr.reduce((acc, value) => { if (acc) { return acc; } if (value.id === '102') { return value; } return undefined; }, undefined); console.log(result); // { id: '102', name: 'Bob' }
同じようにして some()
代替も作れる。
繰り返し
forEach()
代わり。
const arr = [11, 22, 33]; arr.reduce((_, value, index) => { console.log(index, value); }, undefined); // 0 11 // 1 22 // 2 33
初期値を省略すると最初の要素が呼ばれない点に注意。
IDでインデックス
識別子を含むオブジェクトの配列から、その識別子でアクセスできるオブジェクトを作成します。
const arr = [ { id: '101', name: 'Alice' }, { id: '102', name: 'Bob' }, { id: '103', name: 'Charlie' }, ]; const map = arr.reduce((acc, record) => { acc[record.id] = record; return acc; }, {}); console.log(map); // { '101': { id: '101', name: 'Alice' }, // '102': { id: '102', name: 'Bob' }, // '103': { id: '103', name: 'Charlie' } }
オブジェクトを複製
{ ...obj }
や Object.assign()
の代わり。
const obj = { foo: 123, bar: { hoge: 234, fuga: { message: 'Yay!', }, }, }; const obj2 = Object.entries(obj).reduce((acc, [key, value]) => { acc[key] = value; return acc; }, {}); console.log(obj2); // { foo: 123, bar: { hoge: 234, fuga: { message: 'Yay!' } } }
reduce()
より Object.entries()
の方が良い仕事してる気もする。
深いコピーでオブジェクトを複製
const obj = { foo: 123, bar: { hoge: 234, fuga: { message: 'Yay!', }, }, }; const deepCopy = (obj) => Object.entries(obj).reduce((acc, [key, value]) => { if (typeof value === 'object') { acc[key] = deepCopy(value); } else { acc[key] = value; } return acc; }, {}); // 浅いコピー const obj2 = { ...obj }; console.log(obj2); // { foo: 123, bar: { hoge: 234, fuga: { message: 'Yay!' } } } console.log(obj2.bar === obj.bar); // true // 深いコピー const obj3 = deepCopy(obj); console.log(obj3); // { foo: 123, bar: { hoge: 234, fuga: { message: 'Yay!' } } } console.log(obj3.bar === obj.bar); // false
出現要素数を数える
何でもいいいんだけど、試しにHTML要素の要素名で。
const els = [...document.querySelectorAll('*')]; const counts = els.reduce((acc, el) => { const name = el.tagName.toLocaleLowerCase(); if (!acc[name]) { acc[name] = 0; } acc[name] += 1; return acc; }, {}); console.log(counts); // Object { html: 1, head: 1, meta: 23, script: 12, title: 6, link: 32, body: 1, ul: 20, li: 186, a: 223, … }
クラス名とかにしてもおもしろそう。
細かいところ
仕様書の説明
珍しく仕様書にある説明が長いので、ちょっと見てみましょう。
Note 1
callbackfn
should be a function that takes four arguments.reduce
calls the callback, as a function, once for each element after the first element present in the array, in ascending order.
callbackfn
is called with four arguments: thepreviousValue
(value from the previous call tocallbackfn
), thecurrentValue
(value of the current element), thecurrentIndex
, and the object being traversed. The first time that callback is called, thepreviousValue
andcurrentValue
can be one of two values. If aninitialValue
was supplied in the call toreduce
, thenpreviousValue
will be equal toinitialValue
andcurrentValue
will be equal to the first value in the array. If noinitialValue
was supplied, thenpreviousValue
will be equal to the first value in the array andcurrentValue
will be equal to the second. It is aTypeError
if the array contains no elements andinitialValue
is not provided.
reduce
does not directly mutate the object on which it is called but the object may be mutated by the calls tocallbackfn
.The range of elements processed by
reduce
is set before the first call tocallbackfn
. Elements that are appended to the array after the call toreduce
begins will not be visited bycallbackfn
. If existing elements of the array are changed, their value as passed tocallbackfn
will be the value at the timereduce
visits them; elements that are deleted after the call toreduce
begins and before being visited are not visited.
ノート 1
callbackfn
は4つの引数を取る関数になるだろう (should) 。 reduce
はこのコールバックを、関数として、配列に現れる最初の要素以降の各要素ごとに、昇順で呼ぶ。
callbackfn
は以下の4の引数とともに呼ばれる: previousValue
(前回の callbackfn
呼び出しで得られる値)、 currentValue
(現在の要素の値)、 currentIndex
、横断中のオブジェクト。最初にコールバック関数が呼ばれたとき、 previousValue
と currentValue
は以下のいずれかになる。 reduce
に initialValue
が与えられた場合、 previousValue
は initialValue
に等しくなり、 currentValue
は配列の最初の値と等しくなる。 initialValue
が与えられなかった場合、 previousValue
は配列の最初の値と等しくなり、 currentValue
は2番目と等しくなる。配列がひとつも要素を持たず、かつ initialValue
も与えられなかった場合、 TypeError
になる。
reduce
は呼ばれたオブジェクトを直接変化させないが、そのオブジェクトは callbackfn
呼び出しによって変化されてもよい (may) 。
reduce
に処理される要素の範囲は最初の callbackfn
呼び出しの前に設定される。最初の reduce
呼び出しが開始した後に配列へ追加された要素は、 callbackfn
から参照されることはない。配列の既存の要素が変更された場合、 callbackfn
へ与えられる値は reduce
がそれらを参照した時点での値になる。また、 reduce
呼び出しが始まった後、参照より前に削除された要素は、参照されない。
(訳注: 関数が配列要素を visit することを「参照」としました。)
うっ日本語読みづら。
空の配列でエラー
配列の要素ひとつ以上あるいは初期値を与えないとエラーに。
[].reduce(() => {}); // TypeError: Reduce of empty array with no initial value
途中で追加された要素は呼ばれない
const arr = [11, 22, 33]; arr.reduce((_, value, index, original) => { if (index === 0) { original.push(99); } console.log(index, value); }, 0); // 0 11 // 1 22 // 2 33 console.log(arr); // [ 11, 22, 33, 99 ]
途中で変更された要素は変更後の値が利用される
const arr = [11, 22, 33]; arr.reduce((_, value, index, original) => { if (index === 0) { original[1] = 99; } console.log(index, value); }, 0); // 0 11 // 1 99 // 2 33
途中で削除された要素は呼ばれない
const arr = [11, 22, 33]; arr.reduce((_, value, index, original) => { if (index === 0) { delete original[1]; } console.log(index, value); }, 0); // 0 11 // 2 33
途中じゃなくても、削除されて空き枠になっていれば飛ばされます。
空き枠は飛ばされる
まず空き枠、プロパティとして存在しない要素はコールバックで呼ばれません。これは他の繰り返し系配列メソッドと同様。
加えて、初期値を省略した場合に得られる値も、空き枠を飛ばした最初の要素が呼ばれます。
const arr = [, 22, 33,, 55]; arr.reduce((acc, value, index) => { console.log(acc, value, index); return acc; }); // 22 33 2 // 22 55 4
初回コールバック関数呼び出し時の第1引数 acc
は普通 arr[0]
になるんだけど、それが存在しないので、代わりに最初に出現する arr[1]
の 22
になります。第2引数 value
はもちろんその次 33
。
また 33
の次、 arr[3]
もないので、それを飛ばして2回目のコールバック関数呼び出しは arr[4]
の 55
が value
に与えられます。
途中で終了できない
例外投げるとかは別だけど、 for
の break
みたいな機能はないです。
- Repeat, while k < len
(存在しないものを飛ばしながら)単に繰り返すだけ。
使わないでほしい
色々挙げてきたけども、でもやっぱりこの reduce()
はできるだけ使わないでほしいと思います。
- ちょっとややこしくて理解しづらい(初級者が対応できないコード)
- 第 1 引数の関数が長いため第 2 引数の
acc
初期値が遠く、見通しが悪い(保守性が低い) - TypeScript とも相性が悪い(これは他の配列操作も大概だが)
普通の for-of
で繰り返すのが良いかなと。
Before:
const arr = [ { id: '101', name: 'Alice' }, { id: '102', name: 'Bob' }, { id: '103', name: 'Charlie' }, ]; const map = arr.reduce((acc, record) => { acc[record.id] = record; return acc; }, {}); console.log(map);
After:
const arr = […]; const map = {}; for (const item of arr) { map[record.id] = record; } console.log(map);
逆に使いどころは、どうしても for-of
や改行を減らしたいコードゴルフ的な場面とか。第 2 引数を省略する使い方ならまあ正しいかなあという気持ちがありますが、それでも reduce()
が最適解と感じる場面はなかなかないです。
うまく書けるとうおーって気持ち良いんだけど、翌日以降にきっと後悔します。初級者と上級者は使わず中級者くらいの段階の人が使っちゃう感じかなあ。(自分も過去アレだったので反省しています。)
その他
逆に回す reduceRight()
てのもあります。
おしまい
ややこしいやつだけど、おぼえておくとややこしいコードを読むのに役に立つかもしれません。書くのには役立てないでね。
参考
履歴
- 2022-01-16
- 使用を推奨しない方向で文章更新
- 2018-12-23
- 公開