Array.prototype.reduce() の動きを追ってみる。

こんにちは！！株式会社エイチームライフデザインの @TMDM と申します。
この記事は、[Ateam LifeDesign Advent Calendar 2023 シリーズ1](https://qiita.com/advent-calendar/2023/ateam-life-design)の23日目の記事です。

## reduceメソッドってなんなの？
reduceメソッドは、配列の各要素を一つずつ処理して最終的に一つの値を得るために使います。
これを使えば、数の合計、平均の計算、さらにはオブジェクトの組み立てなどができます。

どのようなことが起こっているかは下記に記されています。[公式ページはこちら](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce)

> reduce() は Array インターフェイスのメソッドで、配列のそれぞれの要素に対して、ユーザーが提供した「縮小」コールバック関数を呼び出します。その際、直前の要素における計算結果の返値を渡します。配列のすべての要素に対して縮小関数を実行した結果が単一の値が最終結果になります。
> コールバックの初回実行時には「直前の計算の返値」は存在しません。 初期値が与えらえた場合は、代わりに使用されることがあります。 そうでない場合は、配列の要素 0 が初期値として使用され、次の要素（0 の位置ではなく 1 の位置）から反復処理が開始されます。

...つまり、
`初期値と次のやつで計算 -> さっきの合計と次のやつで計算 -> さっきの合計と次のやつで計算...`
という具合にまとめて(`縮小`)いきます。

## 構文

第二引数まで取れるようですね。


第一引数 : callbackFn
第二引数 : initialValue（初期値）

```js
reduce(callbackFn)
reduce(callbackFn, initialValue)
```

サンプルコードを見てみます。

```js
// 公式から引用
const array1 = [1, 2, 3, 4];
const initialValue = 10;
const sumWithInitial = array1.reduce(
  (accumulator, currentValue) => {
    console.log(accumulator + currentValue) 
    return accumulator + currentValue
  },
  initialValue,
);
console.log(sumWithInitial);
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129861/ae403c73-afe1-b401-fc73-f54bed86023d.png" width="200">

`accumulator`はで言う、`さっきの合計` に当たります。(一番最初は初期値です。)
 - ちなみにaccumulatorは、`acc`と略されがちです。[accumulatorの日本語訳](https://ejje.weblio.jp/content/accumulator))

`さっきの合計と次のやつで計算` で縮小されているのが分かります。


## reduceの基本的な使い方
reduceメソッドは、2つの主要なパーツ、すなわち「コールバック関数」と「初期値」から成り立っています。
実際のコード例を通して、reduceがどのように役立つのかを見ていきましょう。

### 数値配列の合計を出す

#### 初期値なしの場合

```js
const prices = [29.99, 39.99, 19.99, 49.99];
const total = prices.reduce((sum, price) => sum + price);
console.log(total); // 139.96
```

callbackは3回呼ばれます。

ここでのポイントです!!
`initialValue`が指定されていた場合はその値、`initialValue` が無ければ `array[0]` の値を使用します。

```
29.99 + 39.99 = 69.98  // 1回目
69.98 + 19.99 = 89.97  // 2回目
89.97 + 49.99 = 139.96 // 3回目
```

#### 初期値あり

```js

const prices = [29.99, 39.99, 19.99, 49.99];
const total = prices.reduce((sum, price) => sum + price, 10);
console.log(total); // 149.96
```

callbackは4回呼ばれているのがwakarimasu。
```
   10 + 29.99 = 79.98  // 1回目
29.99 + 39.99 = 89.98  // 2回目
69.98 + 19.99 = 99.97  // 3回目
89.97 + 49.99 = 149.96 // 4回目
```

### オブジェクトの配列から特定のプロパティの平均値を出す

#### 初期値なしの場合

```js
const people = [
  { name: "taro", age: 32 },
  { name: "jiro", age: 27 },
  { name: "hanako", age: 25 }
];
const averageAge = people.reduce((sum, person) => sum + person.age) / people.length;
console.log(averageAge); // NaN
```
あれ？<b>NaNが返りました。</b>なんで？？

あ、そうでした。↓
> ここでのポイントです `initialValue`が指定されていた場合はその値、`initialValue` が無ければ `array[0]` の値を使用します。

今回配列の[0]番目はオブジェクトなので、計算できなかったということですね。
初期値に何をおくかが重要ですね！

#### 初期値あり

```js
const people = [
  { name: "taro", age: 32 },
  { name: "jiro", age: 27 },
  { name: "hanako", age: 25 }
];
const averageAge = people.reduce((sum, person) => sum + person.age,0) / people.length;
console.log(averageAge); // 28
```

初期値を0としました。
結果は28と、正しく `people.length` である3で割ることができています。

## 疎配列で使用

疎配列はreduceをスキップしてくれます。

```js
// 公式から引用
console.log([1, 2, , 4].reduce((a, b) => a + b)); // 7
```

しかし、undefinedはスキップしませんので、対応が必要です。

```js
console.log([1, 2, undefined, 4].reduce((a, b) => a + b)); // NaN(>_<)

console.log([1, 2, undefined, 4].reduce((a, b) => {
  if(!b) return a // このような対応が必要
  return a + b
})); // 7
```

## reduce() を使用すべきでない場合

[公式のこちら](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce#reduce_%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%99%E3%81%B9%E3%81%8D%E3%81%A7%E3%81%AA%E3%81%84%E5%A0%B4%E5%90%88)

```js
const names = ["Alice", "Bob", "Tiff", "Bruce", "Alice"];
const countedNames = names.reduce((allNames, name) => {
  const currCount = Object.hasOwn(allNames, name) ? allNames[name] : 0;
  return {
    ...allNames,
    [name]: currCount + 1,
  }; // 目には見えないけど、匿名のオブジェクトを作成して次の計算に使用している
}, {});
```

`return { ...allNames, [name]: currCount + 1 }; ` 
↑この時、新しいオブジェクトを作成しており、前まで(allNames)を上書きしています。
reduceの計算が走るたび、オブジェクトは全体がコピーされます。
そのため、配列が長くなるほどパフォーマンスに影響を与えます。

そんな時はforです。
```js
const names = ["Alice", "Bob", "Tiff", "Bruce", "Alice"];
const countedNames = Object.create(null);
for (const name of names) {
  const currCount = countedNames[name] ?? 0;
  countedNames[name] = currCount + 1;
}
```
countedNamesを予め用意しています。ループの各反復で直接このオブジェクトを更新します。
オブジェクトを直接変更するので、新しいオブジェクトの作成行いません！！！


## お疲れ様でした
この記事では、reduceメソッドが何であるか、基本的な使い方、実用的な例、そして注意すべきポイントを学びました。reduceを使いこなせるようになれば、JavaScriptでの配列操作が格段に楽しくなりそうですね！！
[公式にもあるように](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce#reduce_%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%99%E3%81%B9%E3%81%8D%E3%81%A7%E3%81%AA%E3%81%84%E5%A0%B4%E5%90%88)非効率になってしまう時もあるので、適切なメソッドを選びましょう！