📚

【JavaScript】reduce メソッドを徹底解説

2024/07/01に公開

こんにちは!
ラブグラフエンジニアのひろです。

今回は JavaScript の reduce メソッドについて書いていきます。
reduce メソッドは、配列の各要素に対して関数を適用し、単一の出力値を生成する強力なメソッドです。

このメソッドは、複雑なデータ構造を単純化したり、異なるデータ形式への変換をおこなう場合に特に有効なので、使い方や気を付けるべきポイントなどについて紹介します。

reduce メソッドの基本的な使い方

reduce の基本構文は次の通りです:

array.reduce(function(accumulator, currentValue, currentIndex, array), initialValue)
  • accumulator (累積値)は、配列の各要素を処理する関数によって返された値が蓄積される
  • currentValue は配列内の現在処理中の要素
  • initialValuereduce の最初の呼び出しで accumulator に提供される値で、省略可能

簡単な例

配列の全要素の合計を求めるには、以下のようにします。

const numbers = [1, 2, 3, 4, 5];
const total = numbers.reduce((acc, num) => acc + num, 0);
console.log(total); // 出力: 15

実践的な使用例

配列の要素を組み合わせて新しいオブジェクトを作成

商品のリストから総価格を計算する例:

const items = [
  { name: "博多通りもん", price: 1500 },
  { name: "からし蓮根", price: 1000 },
  { name: "武者返し", price: 300 },
];
const totalPrice = items.reduce((acc, item) => acc + item.price, 0);
console.log(totalPrice); // 出力: 2800

文字列操作の応用

文字列内の各文字の出現回数をカウントする例:

const message = 'hello world';
const letterCount = message.split('').reduce((acc, letter) => {
  acc[letter] = acc[letter] ? acc[letter] + 1 : 1;
  return acc;
}, {});
console.log(letterCount); // 出力: { h: 1, e: 1, l: 3, o: 2, w: 1, r: 1, d: 1 }

データ分析での応用

あるECサイトでは、ユーザーの購買データから月ごとの売上総額を算出する必要がありました。
各トランザクションデータが配列として与えられており、それぞれのトランザクションには金額と日付が記録されています。

reduce メソッドを使用して、この大量のデータから必要な情報を効率的に集計することができます。

const transactions = [
  { amount: 35, date: '2021-01-10' },
  { amount: 45, date: '2021-02-10' },
  { amount: 20, date: '2021-01-22' },
  { amount: 90, date: '2021-01-30' },
  { amount: 60, date: '2021-02-15' },
];

const salesByMonth = transactions.reduce((acc, { amount, date }) => {
  const month = date.substring(0, 7); // "YYYY-MM"
  if (!acc[month]) {
    acc[month] = 0;
  }
  acc[month] += amount;
  return acc;
}, {});

console.log(salesByMonth); // 出力: { '2021-01': 145, '2021-02': 105 }

この方法で、各月の売上を月ごとに効率的に集計でき、さらなる分析やレポート作成の基盤を提供できます。

この事例は、reduce メソッドがデータの集約や分析にどれだけ強力かを示しています。
シンプルながらも非常に強力なこのメソッドを使いこなすことで、コードの効率を大幅に向上させることが可能です。

パフォーマンスへの影響

reduce メソッドは多くの場合、他の配列操作メソッド(例えば mapfilter の組み合わせ)に代わる一つの強力なツールとして機能します。
reduce ひとつで何回ものループを回避し、コードの効率を向上させることができます。

reduce vs map + filter

配列の要素を変換し、条件に基づいてフィルタリングする一般的なタスクを考えてみましょう。

mapfilter を別々に使用する場合、データは2回走査されます:

// 配列の要素を2倍し、5を超えるものだけを出力
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(num => num * 2);
const filtered = doubled.filter(num => num > 5);

これに対して、reduce を使用すると、一回の走査で同じ結果を達成できます:

// 配列の要素を2倍し、5を超えるものだけを出力
const numbers = [1, 2, 3, 4, 5];
const filteredAndDoubled = numbers.reduce((acc, num) => {
  const doubled = num * 2;
  if (doubled > 5) acc.push(doubled);
  return acc;
}, []);

速度も計測してみました。

const array = (new Array(100000)).fill(0).map((_, i)=>{return i});

function mapAndFilter(array) {
  const doubled = array.map(num => num * 2);
  const filtered = doubled.filter(num => num > 5);

  return filtered;
}

function reduce(array) {
  const filteredAndDoubled = array.reduce((acc, num) => {
    const doubled = num * 2;
    if (doubled > 5) acc.push(doubled);
    return acc;
  }, []);

  return filteredAndDoubled;
}

const mapAndFilterStartTime = performance.now();
mapAndFilter(array)
const mapAndFilterEndTime = performance.now();

const reduceStartTime = performance.now();
reduce(array)
const reduceEndTime = performance.now();

console.log('--- map and filter ---')
console.log(mapAndFilterEndTime - mapAndFilterStartTime);

console.log('--- reduce ---')
console.log(reduceEndTime - reduceStartTime);

--- map and filter ---
2.0999999046325684
--- reduce ---
1.1000001430511475

約半分の速度で実行が完了しています。

この例から分かるように、reduce は計算資源を節約し、コードのパフォーマンスを向上させるための効率的な選択肢となり得ます。

パフォーマンスに関する考慮事項

しかし、reduce の使用は適切な場面を選ぶ必要があります。
reduce が強力である一方で、複雑なロジックや多くの条件が絡む場合はコードの可読性を低下させることがあります。

特に、累積値の管理が複雑になると、エラーの原因となり得るため、シンプルなタスクには mapfilterforEach など他のメソッドが適している場合もあります。

最適な使用シナリオ

reduce は次のようなケースで役に立ちます

  • 複数の操作を一つのループで完了させたい場合
  • 配列から単一の値(数値、オブジェクト、配列など)を導出したい場合
  • パフォーマンスが重要な大規模データ処理が必要な場合

よくある間違いとその対処法

reduce メソッドを使用する際には、いくつかの注意点があります。
これらの注意点と対処法を理解し、 reduce の適切な使い道を見つけましょう。

初期値を省略した場合のエラー

reduce を使う際に最もよく見られるミスは、初期値を省略することです。
特に配列が空の場合、初期値なしで reduce を呼び出すとエラーが発生します。

const numbers = [];
const total = numbers.reduce((acc, num) => acc + num);
=> TypeError: Reduce of empty array with no initial value

この問題を避けるためには、常に初期値を提供することが重要です。

const numbers = [];
const total = numbers.reduce((acc, num) => acc + num, 0); // 正しく動作し、出力は 0

非同期処理との組み合わせの問題

reduce メソッドは同期的に動作します。
そのため、reduce 内で非同期関数(例えば、APIからのデータ取得をおこなう fetch など)を直接呼び出すとエラーが発生します。

const areaIdToName = await areaIds.reduce(async (acc, areaId) => {
  const areaName = await fetchAreaNameByAreaId(areaId);
  acc = acc.concat(areaName);

  return acc;
}, []);
=> Uncaught TypeError: acc.concat is not a function // acc の中身が Promise オブジェクトになるので、 concat メソッドが存在しないというエラーになる

このようなケースでは、非同期処理が必要な処理を事前におこない、取得後のデータを reduce で集約する、というふうにステップを分ける必要があります。

まとめ

reduce メソッドはその多機能性により、様々な問題解決に活用できます。
使い所の難しいメソッドではありますが、この記事が reduce メソッドの理解を深め、より実践的な使用を促進するきっかけになれば幸いです。

ラブグラフのエンジニアブログ

Discussion