「map や filter は使いこなしているけれど、reduce は難しそう…」と感じていませんか?
JavaScriptの配列メソッドの中でも、特に強力で多機能なのがリデューサーです。リデューサーをマスターすれば、配列の合計値計算はもちろん、複雑なデータ変換やグループ化まで、驚くほどシンプルに記述できるようになります。
この記事では、「リデューサーとは何か?」という基本から、具体的な使用例、メリットと注意点までを、実例とともに解説しています。
これを読めば、あなたのJavaScriptコードは劇的に進化するはずです!
リデューサーとは何か?
JavaScriptの「リデューサー(Reducer)」とは、値やアクションなどのたくさんの入力を受け取り、集計値や新しい状態など1つの出力を返す関数です。
主に配列の操作に使われる、非常に強力な機能で、配列の要素を左から右へ順番に処理し、最終的に「一つの値」にまとめ上げるために使う関数です。
JavaScriptの状態管理ライブラリ「Redux」やReactの「useReducer」などでもこのリデューサーの概念が使われています。
reduceメソッド
リデューサーとは複数の入力値を1つに要約する関数です。
JavaScriptのリデューサーといえばreduceメソッドです。(他にもreduceRight, find, someなどがあります)
reduceメソッドの基本構文
reduceメソッドの基本構文は以下のようになっています。
array.reduce((accumulator, currentValue, index, array) => {
// 処理して accumulator を返す
}, initialValue);
4つの引数と、初期値をとることができます。
- accumulator:これまでの「集約した結果」。次の呼び出しで最初の引数になる。accが使われることが多い。
- currentValue:現在処理している要素。curやvalが使われることが多い。
- index:現在処理している要素のインデックス(省略可)
- array:reduceを呼び出した元の配列自体(省略可)
- initialValue:初期値(省略可)。省略すると配列の最初の要素が初期値になり、処理は2番目の要素から始まる。
予期せぬ挙動を防ぐためにも、基本的には初期値を指定することが推奨されます。
※例えば、空の配列に対して初期値なしでreduce()を実行するとエラーになります。
reduceのより詳しい解説については下記をご参考ください。
> 【JavaScript】reduceメソッドとは何か?第1, 第2, 第3, 第4引数の意味や初期値の設定方法を実例で解説
リデューサーの例1:合計値の算出
例えば、数値の入った配列を渡すと、その合計を返すリデューサーは以下のようになります。
const nums = [1, 2, 3, 4];
const sum = nums.reduce((acc, cur) => acc + cur, 0); // 10初期値で0が設定してあることがポイントです。
- 最初のループでは、acc=0, cur=1となり、結果は1となります。
- 2回目のループでは、acc=1, cur=2 となり、結果は3です。
- 3回目のループでは、acc=3, cur=3 となり、結果は6です。
- 4回目のループでは、acc=6, cur=4 となり、最終的な出力は10となります。
複数の数値が最終的に1つに集約される。これがリデューサーの概念です。
リデューサーの例2:単一の結果を返す
なんらかの処理をして単一の結果を返すのもリデューサーです。
例えば、['apple','banana','apple','orange','banana']という配列を受け取り、各要素が何回出現しているかをカウントして返すのもリデューサーの処理になります。
const fruits = ['apple','banana','apple','apple'];
const counts = fruits.reduce((acc, f) => {
acc[f] = (acc[f] || 0) + 1;
return acc;
}, {});
// => { apple: 3, banana: 1 }
初期値は空のオブジェクト{ }を指定しています。
またacc[f] || 0は論理演算子で、acc[f]があればその値を使い、なければ0とする処理です。
- 1回目のループで、
acc={}、f=appleとなります。accの中にappleは無いので、acc[f]=undefinedとなり、0+1が計算され、{apple:1}が返ります。 - 2回目のループは、
acc={'apple':1},f='banana'となります。accの中にbananaは無いので、acc[f]=undefinedとなり、0+1が計算され、{apple:1, banana:1}が返ります。 - 3回目のループは、
acc={'apple':2, 'banana':1},f='apple'となります。accの中にappleが存在するので、acc[f]=1となり、1+1が計算され、{'apple':2, 'banana':1}が返ります。 - 4回目のループは、
acc={'apple':2, 'banana':1},f='apple'となります。accの中にappleが存在するので、acc[f]=2となり、2+1が計算され、最終的に{'apple':3, 'banana':1}が返ります。
上記でacc[f]はオブジェクトの中の指定したプロパティの値にアクセスする記法です。
例えば、オブジェクトobj = {'a':5, 'b':10 }において、その中の要素であるプロパティにアクセスするには obj[プロパティ名]とします。obj['a']とするとその値である5を取得することができます。
また、obj[プロパティ名]=代入値 とすることで、指定したプロパティの値を上書きすることができます。
obj.プロパティ名でアクセスするドット記法に対して、[ ]を使うため、ブラケット記法といいます。
リデューサーの例3:配列をオブジェクトに変換する
例2と同様に、配列をオブジェクトに変換する処理です。
ポイントは、配列を非破壊で処理し、新しいデータを作れるということです。
以下では、peopleという変数に入っている、各人を、都市名を基準にしたオブジェクトに変換します。
const people = [
{ name: 'Taro', city: 'Tokyo' },
{ name: 'Hanako', city: 'Osaka' },
{ name: 'Jiro', city: 'Tokyo' }
];
// 街ごとに人をグループ化する
const groupedByCity = people.reduce((acc, person) => {
const cityKey = person.city;
// cityKey (例: 'Tokyo') がまだアキュムレーターにない場合、空の配列で初期化する
if (!acc[cityKey]) {
acc[cityKey] = [];
}
// その街の配列に現在の人物を追加する
acc[cityKey].push(person.name);
// 新しいアキュムレーターを返す(ここではオブジェクト自体を返す)
return acc;
}, {}); // 初期値は {} (空のオブジェクト)
console.log(groupedByCity);
/* 出力:
{
Tokyo: ['Taro', 'Jiro'],
Osaka: ['Hanako']
}
*/リデューサーを使うメリット
リデューサーを使う大きなメリットは以下の3つです。
- 処理が集約できる
forループやforEachを使って複数の処理を書いていたものを、reduce()一つにまとめることができ、コードがシンプルで読みやすくなります。 - 汎用性が上がる
合計、平均、グループ化、平坦化(ネストされた配列を一段にする)、重複の削除など、配列から単一の結果を導き出すあらゆる場面で利用できます。 - 非破壊処理ができる
配列を直接変更しない(非破壊的な処理)ため、データの不変性(イミュータビリティ)を保ちやすく、予期せぬバグを防ぎやすくなります。
リデューサーの注意点
リデューサーを使うときは以下に注意が必要です。
- 複雑なロジックは避ける
リデューサーは一つの値への集約に適していますが、ロジックが複雑になりすぎると可読性が低下します。その場合は、map()やfilter()など、他の配列メソッドとの組み合わせを検討します。 - 初期値を指定する
常に初期値を指定するように心がけてください。特に、結果が配列やオブジェクトになる場合は、初期値として[]や{}を指定しないとエラーや予期せぬ挙動の原因になります。 - returnでアキュムレーター(acc)を返す
reduceの処理でアキュムレーター(acc)をreturnしないと、次の処理でundefinedとなり、エラーが発生します(エラー:TypeError: Cannot read properties of undefined) - アキュムレーター(acc)は変更しない
リデューサー関数内では、最終的なアキュムレーターを返すために、アキュムレーターを直接変更(ミューテート)しないように注意が必要です。新しいオブジェクトや配列を作成して返すことが、不変性を保つ原則(特に状態管理においては必須)です。
以下のように、直接accを編集することは避けるべきです。accを直接いじり、破壊処理をしています。
people.reduce((acc, person) => {
acc[person.city] = acc[person.city] || [];
acc[person.city].push(person.name);
return acc;
}, {}); 正しくは、スプレッド構文を使ってオブジェクトを生成します。
const groupedByCityImmutable = people.reduce((acc, person) => {
const cityKey = person.city;
return {
...acc, // 既存のaccのコピー
[cityKey]: [...(acc[cityKey] || []), person.name] // 既存の配列もコピーして要素を追加
};
}, {});reduce以外のリデューサー
reduce以外にもリデューサーとなる関数、あるいはリデューサーのような処理をする関数がいくつかあります。
例えば以下のメソッドです。
reduceRight()
reduce の「右から左へ」バージョンです。配列を逆順に処理したいときに使います。
文字列の連結やスタック処理など、「逆順」が意味を持つ場合に便利。
const letters = ['a', 'b', 'c'];
const result = letters.reduceRight((acc, cur) => acc + cur, '');
console.log(result); // "cba"map() + join()
map() + join()は厳密には reduce ではありませんが、あるデータを変換し、1つにまとめるという流れをするため、「分解 → 再構築」という reduce 的操作の一種です。
const nums = [1, 2, 3];
const doubledSum = nums.map(n => n * 2).join('-');
console.log(doubledSum); // "2-4-6"every(), some()
every()は全ての要素が条件に一致した場合にtrueを返します。
some()は要素のうち一つでも条件に一致した場合はtrueを返します。
このevery()とsome()は、内部的には「配列を走査して単一の論理値にまとめる」ので、
リデューサーの特殊形といえます。
const ages = [18, 20, 25, 17];
const allAdults = ages.every(age => age >= 18); // 全員18歳以上か?
const hasMinor = ages.some(age => age < 18); // 未成年がいるかどうか?
console.log(allAdults); // false
console.log(hasMinor); // true
find(), findLast(), findIndex()
find(), findLast(), findIndex()は、配列全体を条件に合う1つの値(またはインデックス)に絞り込む操作なので、reduceの早期終了版のような動きをします。
find()は、条件を満たした最初の要素の値を取得します。
const numbers = [5, 12, 8, 130, 44];
const found = numbers.find(element => element > 10);
console.log(found); // 12findLast()は、後方から検索をかけ条件を満たした最後の要素の値を取得します。
const numbers = [5, 12, 8, 130, 44];
const foundLast = numbers.findLast(element => element > 10);
console.log(foundLast); // 44 (130 の次、末尾から見て最初に見つかる)findIndex()は条件を満たした最初の要素のインデックスを取得します。
const numbers = [5, 12, 8, 130, 44];
const index = numbers.findIndex(element => element > 10);
console.log(index); // 1 (値 12 のインデックス)flat()
ネストした配列を平たん化する処理です。同じことがreduce()でもできますが、flat()の方が可読性が高く最適です。
const arr = [[1, 2], [3, 4]];
console.log(arr.flat()); // [1, 2, 3, 4]flatMap()
flatMap()は、flat(1)とmap()を組み合わせたメソッドです。
const words = ["Hello", "World"];
const letters = words.flatMap(word => word.split(''));
console.log(letters); // ['H','e','l','l','o','W','o','r','l','d']

