我是阿傑,曾經聽雪瑞學姐說過 reduce()
好好用、整理 API 好棒棒,我當下心想這麼好用的東西要是不會用可是要吃大虧了呢 (大媽心態??),貪小便宜的我立刻打開 MDN 搜尋這個咩色並帶著不知哪來的自信閱讀了起來,在過了約莫 10 分鐘後,我突感一陣暈眩並當機立斷地把分頁關掉,接著就乖乖地帶著電腦走去跟學姐跪求教學!
那我們今天就來介紹一下這個新人殺手 - Array.prototype.reduce
,不要擔心太多,我們勇敢地往下看...
我們會分成以下幾個部分介紹:
當你需要把一個陣列整理成任一型態的值,即可考慮使用,什麼意思呢?我們可以把 reduce 想像成逐漸變成某個東西,也就是我們可以把一個陣列逐漸變成下列的結果之一:
最常聽到的例子可能就是把一個包含數字的陣列整理成一個數字,例如"加總":
[1,2,3,4,5] => 15
你可以選擇是否給這個 reduce()
一個 初始值 (initial value),這會影響它的行為模式跟結果。
reduce((previousValue, currentValue, currentIndex, array) => { * ... *}, initialValue)
reduce(callbackFn, initialValue)
reduce(function(previousValue, currentValue, currentIndex, array){ * ... * }, initialValue)
reduce()
的第一個參數是 callback, 第 2 個參數為選擇性的 (optional) initialValue
。
callback
這個 callback 又被稱為 reducer ,reduce
會按照陣列元素的順序依次呼叫這個 callback,但被呼叫的最大次數會因是否有提供 initialValue
而不同,換句話說,如果陣列有 5 個元素,那這個 callback 最多會被呼叫 5 次。
callback 每次都會被帶入 4 個參數呼叫,分別是 previousValue
、currentValue
、currentIndex
及 array
。
callback 每次都必須回傳一個值出去,這個值會成為下個 callback 的 previousValue
,直到陣列被遍歷完畢。
previousValue
前一次 callback 回傳的值,又稱為 accumulator (累加器)。
如果有提供 initialValue
,那第 1 次 callback 的 previousValue
會是 initialValue
;否則會是陣列的第 1 個元素 (array[0]
)。
currentValue
陣列當前的元素。
如果有提供 initialValue
,那第 1 次 callback 的 currentValue
會是陣列的第 1 個元素 (array[0]
);否則會是陣列的第 2 個元素 (array[1]
)。
currentIndex
陣列當前元素的索引 (index).
如果有提供 initialValue
,那第 1 次 callback 的 currentIndex
會是 0
;否則會是 1
array
呼叫 reduce()
的陣列本身 (被遍歷的陣列本身),不論 reduce()
當下遍歷到哪個元素。
initialValue
(可選的)它會影響第 1 次 callback 帶入的參數,如果有提供 initialValue
:
previousValue
會是 initialValue
currentValue
會是陣列的第 1 個元素 (array[0]
)currentIndex
會是 0如果沒有提供 initialValue
:
previousValue
會是陣列的第 1 個元素 (array[0]
)currentValue
會是陣列的第 2 個元素 (array[1]
)currentIndex
會是 1回傳一個被 callback (reducer) 整理過的值,這個值可以是任何型態,例如數字或物件等...。
不會變動到原陣列。
要弄懂 reduce()
就必須要清楚知道 initialValue
、previousValue
跟 currentValue
之間的關係。
可以把 previousValue
看做一個累加器 (accumulator),這個累加器可以是任何值 (例如一個數字或一個物件),它會持續變化 (reduce) 並不斷地被往下傳遞,也就表示我們回傳出去的值會變成下次 callback 的 previousValue
。
要注意 previousValue
除了會不斷地變化外,它跟 currentValue
及 currntIndex
的初始值都會受到 initialValue
的影響 (可以參考上面的參數說明),我們使用範例的 Example 1、Example 2 來說明參數帶入的狀況:
initialValue
時:最後的回傳值 15
會被 reduce()
回傳出去
initialValue
時:最後的回傳值 14
會被 reduce()
回傳出去
只要能掌握這幾個參數的變化,redue
不只可以拿來做加總之類的用途,它還可以用來整理這個陣列,讓其變成我們想要的型態,例如範例的 Example 3,我們使用了一個空陣列作為 initialValue
,它便會成為第 1 次 callback 的 previousValue
,之後被不斷地傳遞並變化成我們想要的結果。
initialValue
)const numbers = [1, 2, 3, 4, 5]
const sum = numbers.reduce((currentSum, currentNumber) => currentSum + currentNumber)
console.log(sum)
// 15
currentSum
為這個 reduce()
的累加器 (accumulator),它的初始值為 numbers
的第 1 個元素 - 1
,而currentNumber
會從 numbers
的第 2 個元素開始 - 2
。
因此 reduce
會執行 4 次callback,而每次 callback 回傳的值都會成為下一次的 currentSum
。
initialValue
)const numbers = [1, 2, 3, 4, 5]
const sum = numbers.reduce((currentSum, currentNumber) => currentSum + currentNumber, -1)
console.log(sum)
// 14
currentSum
為這個 reduce()
的累加器 (accumulator),它的初始值為 initialValue
- -1
,而currentNumber
會從 numbers
的第 1 個元素開始 - 1
。
因此 reduce
會執行 5 次callback,而每次 callback 回傳的值都會成為下一次的 currentSum
。
const datas = [
{ name: 'Alejo', ages: 21 },
{ name: 'Emma', ages: 32 },
{ name: 'Pedro', ages: 42 },
{ name: 'Samantha', ages: 18 },
]
const ages = datas.reduce((ages, data) => {
const agesObj = { [data.name]: data.ages }
ages.push(agesObj)
return ages
},[])
console.log(ages)
// [
// { Alejo: 21 },
// { Emma: 32 },
// { Pedro: 42 },
// { Samantha: 18 }
// ]
這個 initialValue
為一個空陣列,代表這個累加器會是一個陣列,而最後 reduce
回傳的也會是一個陣列。
const people = [
{ name: 'Emma', gender: 'female' },
{ name: 'Pedro', gender: 'trans' },
{ name: 'Collin', gender: 'male' },
{ name: 'Ted', gender: 'male' },
{ name: 'Ginger', gender: 'trans' }
]
function groupBy(objArray, property) {
return objArray.reduce((groupedObj, obj) => {
const key = obj[property]
const currentGroup = groupedObj[key] ?? []
return { ...groupedObj, [key]: [...currentGroup, obj] }
}, {})
}
const peopleByGender = groupBy(people, 'gender')
console.log(peopleByGender)
// {
// female: [{ name: 'Emma', gender: 'female'}],
// male: [
// { name: 'Collin', gender: 'male' },
// { name: 'Ted', gender: 'male' }
// ],
// trans: [
// { name: 'Pedro', gender: 'trans' },
// { name: 'Ginger', gender: 'trans' }
// ]
// }
這個 groupBy()
會利用指定的屬性對包含物件的陣列做分類,最後回傳一個物件,其屬性都是跟據指定屬性的值來做分類。
在這裡,我們使用 people
的每個物件裡的 gender
來做分類,也就是最後整理好的物件會有 female
、male
、trans
3 個屬性,每個屬性的值都會是一個陣列,包含著相關的物件。
請注意 callback 定義時的參數順序,依序應為 previousValue
、currentValue
、currentIndex
、array
,假設你只想使用 currentIndex
而不使用 currentValue
,你仍然需在前面定義 currentValue
,也可以增加一個底線以利閱讀,例如這樣:
array.reduce((previousValue, _currentValue, currentIndex) => { /* ... */ })
雖然 reduce()
很好用,但它在閱讀上並不是那麼直覺,尤其對新手來說會有點迷惑,我們可以多多利用其他咩色做到相同的事,如果沒辦法就應該讓參數的命名更加語義化來增加可讀性。
如果陣列沒有元素 (如果只有 empty slot 也會被視作沒有元素) 也沒有提供 initialValue
的話,會丟出一個 TypeError 的錯誤。
reduce()
無法跟其他可傳入 callback 的咩色一樣指定其 callback 的 this
。
有一點值得注意的是,雖然 reduce()
不會變動到原陣列,但我們傳進去的 callback 卻有可能 ,而陣列元素被遍歷的範圍在第一次呼叫 callback 前就已經確立好了 (也就是 reduce()
被呼叫後但 callback 尚未被呼叫),因此有可能會發生以下的狀況:
上述這種高併發 (concurrent) 的更動會導致程式碼非常難以閱讀,非常不建議使用 (除非有特殊的情境)。
Array.prototype.reduce(callbackfn[,initialValue])
reduce()
的演算法並沒有要求呼叫它的一定要是一個陣列,可以從步驟 1 跟 Note 2 得知,為了方便解釋,這邊一律使用陣列來說明。
演算法的前 3 個步驟都是用來做一些前置處理,包括轉型、確認長度、確認參數是否為一個 function 等...。
在步驟 4 會看到當陣列長度為 0 時且又沒有提供 initialValue
時就會丟出 1 個 TypeError,驗證了上面注意事項所說。
步驟 7 會看到當有提供 initialValue
時,initialValue
會被指派給 accumulator
(previousValue),驗證了上面參數的說明。
步驟 8 會看到如果沒有提供 initialValue
時,會從索引 0 開始找有無相對應的元素並指派給 accumulator
,因為這邊不會是空陣列,所以第 1 個元素 (array[0]
) 就會成為 accumulator
的初始值,驗證了上面的參數說明。
步驟 9 會看到遍歷的次數在 callback 第 1 次呼叫前就已經決定好了,而且每次的 callback 呼叫前都會先檢查相對應的屬性 (陣列索引) 是否存在, 如果不存在則不會呼叫,驗證了上面注意事項對高併發之 callback 的說明。
在步驟 9 還會看到 callback 被呼叫時的 this
為 undefined
,驗證了上面所說的 - 我們無法指定 callback 的 this
。
我們來驗證一下 reduce()
是否如規範所說的被做成通用的一個方法:
const fakeArray = {
0: 1,
1: 2,
2: 3,
3: 4,
length: 4
}
const sum = Array.prototype.reduce.call(fakeArray, (sum, currentNum) => sum + currentNum)
console.log(sum)
// 10
只要清楚知道 accumulator 會被如何傳遞,以及 initialValue
是如何影響這些參數的初始值, reduce()
應該就不會讓人這麼迷惑。
而使用 reduce()
這類咩色還有一個很重要的地方,就是可以減少變數的宣告,不但可以提高閱讀性,還可以減少變數污染之類的問題。
最後,希望大家可以開心地使用各種咩色,體驗它帶給你的便利,祝大家歸剛沒煩惱。
特地來留一下reduce
以前新人看到reduce真的超就崩潰啦
但自從一次茅塞頓開之後
真的就是好用而已
要離職之前還故意留一堆reduce的code給後人(誤
印象中我出面試題還故意出這個,要考一下是不是真的懂