JavaScript reduce 在做什麼?
陣列方法有很多種,包括 `forEach`、`map`、`filter` 等等,其中 `reduce`算是比較複雜且容易讓人感到困惑的一種方法,因此這篇文章會介紹 JavaScript `reduce` 的功能與基本應用。
## 簡介
關於 `reduce` [MDN](https://firebase.google.com/docs/firestore/?hl=zh-CN) 的定如下:
> The reduce() method executes a reducer function (that you provide) on each member of the array resulting in a single output value.
簡單來說就是 `reduce` 方法跟其他陣列方法(例如:`map`、`filter`)的差別是它會 return 一個值,而不是一個新陣列,這會連帶使 `reduce` 的語法結構跟邏輯與其他方法不太相同。
## reduce 的基礎語法
``` js
Array.reduce(callback[accumulator, currentValue, currentIndex, array], initialValue)
```
看起來有點複雜,其實就是以下這五個參數:
- accumulator:經由個別 `currentValue` 加總的累計值
- currentValue:`Array` 的個別 item
- currentIndex:`Array` item 的索引
- array:呼叫該 `Array method` 的陣列
- initialValue:預設值,放在 `function` 的最後方,非必填
其中比較特別的是 `accumulator` 與 `initialValue`,在其他的陣列方法裡也少見。用文字敘述可能會不太清楚這些參數的意思,這裡用最簡單的陣列數值加總為例,並預設兩種不同狀況:
### 1. 未提供 `initialValue`(預設值)
``` js
const arr = [1, 2, 3, 4, 5];
const reduceArr = arr.reduce((accumulator, currentValue) => {
return accumulator + currentValue
});
console.log(reduceArr); // 15
```
上述範例若未提供預設值,`accumulator`(累計值)就會取陣列的第一個元素也就是 1,而 `currentValue` 就會從陣列的第二個值開始 loop。我們可以執行 `console` 確認數值為何?
``` js
const arr = [1, 2, 3, 4, 5];
const reduceArr = arr.reduce((accumulator, currentValue) => {
console.log(accumulator); // 1, 3, 6, 10
console.log(currentValue); // 2, 3, 4, 5
return accumulator + currentValue
});
```
可以清楚看到 `accumulator` 是從 1 開始接收 `currentValue` 的值並開始累計,而 `currentValue` 就從 2 開始 loop,加總方法如以下表格:
![images](/img/176cyd.png)
### 2. 有提供 `initialValue`(預設值)
``` js
const arr = [1, 2, 3, 4, 5];
const reduceArr = arr.reduce((accumulator, currentValue) => {
return accumulator + currentValue
}, 0);
console.log(reduceArr); // 15
```
可以看到加上預設值 0,雖然 return `value` 跟前面範例一樣是 15,但內部結構已不相同。這裡一樣執行 `console` 確認數值為何?
``` js
const arr = [1, 2, 3, 4, 5];
const reduceArr = arr.reduce((accumulator, currentValue) => {
console.log(accumulator); // 0, 1, 3, 6, 10
console.log(currentValue); // 1, 2, 3, 4, 5
return accumulator + currentValue
}, 0);
```
這裡的 `accumulator` 變成從 0 開始計算,後面接續 `currentValue` 的值累計。而 `currentValue` 會從陣列的第一個值開始 loop。
![images](/img/27oy3m.png)
那如果預設值不為 0 而是 5,又會有什麼不同的結果呢?
``` js
const arr = [1, 2, 3, 4, 5];
const reduceArr = arr.reduce((accumulator, currentValue) => {
console.log(accumulator); // 5, 6, 8, 11, 15
console.log(currentValue); // 1, 2, 3, 4, 5
return accumulator + currentValue
}, 5);
```
這裡的 `accumulator` 變成從 5 開始計算,後面接續 `currentValue` 的值累計。return `value` 變成 20。
![images](/img/37q2wj.png)
## 更多 reduce 應用
`reduce` 除了可以處理數字累計之外,還可以做更廣泛的應用。
### 合併陣列
``` js
const arr = [['a', 'b'], ['c', 'd'], ['e', 'f']];
const reduceArr = arr.reduce((accumulator, currentValue) => {
return accumulator.concat(currentValue);
}, []);
console.log(reduceArr); // ['a', 'b', 'c', 'd', 'e', 'f'];
```
可以看到當預設值為空陣列時,`reduce` 如何透過累計的方式 return 一個合併後的新陣列。
### 計算相同字串的數量並以物件呈現
``` js
const arr = ['a', 'a', 'b', 'c', 'c', 'c', 'e'];
const reduceArr = arr.reduce((accumulator, currentValue) => {
if(accumulator[currentValue]) {
accumulator[currentValue] ++;
} else {
accumulator[currentValue] = 1;
}
return accumulator;
}, {});
console.log(reduceArr); // { a: 2, b: 1, c: 3, e: 1 }
```
這一段主要在計算變數 `arr` 內值出現的次數。當預設值為一空物件時,`reduce` loop 過變數 `arr` 後會將 `currentValue` 累計在已繼承預設值的 `accumulator` 上並產生一個新物件。
如果用一般的陣列方法可以這麼寫(以 `forEach` 為例)
``` js
const arr = ['a', 'a', 'b', 'c', 'c', 'c', 'e'];
const obj = {};
arr.forEach(function(char) {
if(obj[char]) {
obj[char] ++;
} else {
obj[char] = 1;
}
})
console.log(obj) // { a: 2, b: 1, c: 3, e: 1 }
```
也可以達到一樣的效果。
其實用這兩種方法的邏輯是一致的,但 `reduce` 的 `initialValue` 可以設為任一型態(數值、物件、陣列),而 `accumulator` 可繼承 `initialValue` 的型態並接收 `currentValue` 計算後的結果。因此雖然程式碼看似沒有簡化很多,但若需要對陣列操作進行比較細膩的比對與計算的話寫起來會較直覺且靈活一些。
## 總結
- 在執行 `reduce` 時需先確認有沒有放 `initialValue`,因為這會直接影響 return 的結果。
- `accumulator`(累計值)是一個特殊的存在,它會反應 `currentValue` 的累計值,這在其他陣列方法比較少見。
- 承上,因為有 `accumulator` 這個參數,使 `reduce` 可以針對陣列的 item 與 item 之間執行一些比較細膩的比對與操作。
## 參考文章
* [MDN reduce](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce)
* [One reduce() to rule them all — How to use reduce in JavaScript](https://levelup.gitconnected.com/one-reduce-to-rule-them-all-504e1b790a83)
* [Finally Understand the JavaScript Reduce Method](https://alligator.io/js/finally-understand-reduce/)