iT邦幫忙

2022 iThome 鐵人賽

DAY 13
1

Abstract

我是阿傑,曾經聽雪瑞學姐說過 reduce() 好好用、整理 API 好棒棒,我當下心想這麼好用的東西要是不會用可是要吃大虧了呢 (大媽心態??),貪小便宜的我立刻打開 MDN 搜尋這個咩色並帶著不知哪來的自信閱讀了起來,在過了約莫 10 分鐘後,我突感一陣暈眩並當機立斷地把分頁關掉,接著就乖乖地帶著電腦走去跟學姐跪求教學!

那我們今天就來介紹一下這個新人殺手 - Array.prototype.reduce,不要擔心太多,我們勇敢地往下看...

不多慮的才是聰明人

我們會分成以下幾個部分介紹:

  • 使用時機
  • 語法
  • 說明
  • 範例
  • 注意事項
  • ECMAScript
  • 結論

使用時機

當你需要把一個陣列整理成任一型態的值,即可考慮使用,什麼意思呢?我們可以把 reduce 想像成逐漸變成某個東西,也就是我們可以把一個陣列逐漸變成下列的結果之一:

  • 陣列 -> 1 個 String
  • 陣列 -> 1 個 Number
  • 陣列 -> 1 個 Array
  • 陣列 -> 1 個 Object

最常聽到的例子可能就是把一個包含數字的陣列整理成一個數字,例如"加總":

[1,2,3,4,5] => 15

你可以選擇是否給這個 reduce() 一個 初始值 (initial value),這會影響它的行為模式跟結果。


語法

  • Arrow function (直接定義箭頭函式)
reduce((previousValue, currentValue, currentIndex, array) => { * ... *}, initialValue)
  • callback (直接傳入回呼函式)
reduce(callbackFn, initialValue)
  • Inline Callback (直接定義匿名函式)
reduce(function(previousValue, currentValue, currentIndex, array){ * ... * }, initialValue)

參數

reduce() 的第一個參數是 callback, 第 2 個參數為選擇性的 (optional) initialValue

callback

這個 callback 又被稱為 reducer ,reduce 會按照陣列元素的順序依次呼叫這個 callback,但被呼叫的最大次數會因是否有提供 initialValue 而不同,換句話說,如果陣列有 5 個元素,那這個 callback 最多會被呼叫 5 次。

callback 每次都會被帶入 4 個參數呼叫,分別是 previousValuecurrentValuecurrentIndexarray

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

Return value

回傳一個被 callback (reducer) 整理過的值,這個值可以是任何型態,例如數字或物件等...。

Mutability

不會變動到原陣列。


說明

要弄懂 reduce() 就必須要清楚知道 initialValuepreviousValuecurrentValue 之間的關係。

可以把 previousValue 看做一個累加器 (accumulator),這個累加器可以是任何值 (例如一個數字或一個物件),它會持續變化 (reduce) 並不斷地被往下傳遞,也就表示我們回傳出去的值會變成下次 callback 的 previousValue

要注意 previousValue 除了會不斷地變化外,它跟 currentValuecurrntIndex 的初始值都會受到 initialValue 的影響 (可以參考上面的參數說明),我們使用範例的 Example 1、Example 2 來說明參數帶入的狀況:

  • Example 1 - 沒有 initialValue 時:

13.2

最後的回傳值 15 會被 reduce() 回傳出去

  • Example 2 - 有 initialValue 時:

13.3

最後的回傳值 14 會被 reduce() 回傳出去

只要能掌握這幾個參數的變化,redue 不只可以拿來做加總之類的用途,它還可以用來整理這個陣列,讓其變成我們想要的型態,例如範例的 Example 3,我們使用了一個空陣列作為 initialValue,它便會成為第 1 次 callback 的 previousValue ,之後被不斷地傳遞並變化成我們想要的結果。


範例

Example 1 - 基礎用法 - 加總 (沒有 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

Example 2 - 基礎用法 - 加總 (有 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

Example 3 - 將陣列內的物件整理成不同形式的物件

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 回傳的也會是一個陣列。

Example 4 - 利用物件的屬性做分類

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 來做分類,也就是最後整理好的物件會有 femalemaletrans 3 個屬性,每個屬性的值都會是一個陣列,包含著相關的物件。


注意事項

請注意 callback 定義時的參數順序,依序應為 previousValuecurrentValuecurrentIndexarray,假設你只想使用 currentIndex 而不使用 currentValue,你仍然需在前面定義 currentValue,也可以增加一個底線以利閱讀,例如這樣:

array.reduce((previousValue, _currentValue, currentIndex) => { /* ... */ })

雖然 reduce() 很好用,但它在閱讀上並不是那麼直覺,尤其對新手來說會有點迷惑,我們可以多多利用其他咩色做到相同的事,如果沒辦法就應該讓參數的命名更加語義化來增加可讀性。

如果陣列沒有元素 (如果只有 empty slot 也會被視作沒有元素) 也沒有提供 initialValue 的話,會丟出一個 TypeError 的錯誤。

reduce() 無法跟其他可傳入 callback 的咩色一樣指定其 callback 的 this

有一點值得注意的是,雖然 reduce() 不會變動到原陣列,但我們傳進去的 callback 卻有可能 ,而陣列元素被遍歷的範圍在第一次呼叫 callback 前就已經確立好了 (也就是 reduce() 被呼叫後但 callback 尚未被呼叫),因此有可能會發生以下的狀況:

  • 在這之後才被添加 (appended) 進去的元素不會被 callback 遍歷到
  • 如果原先存在的元素被變動到了, 那當 callback 遍歷到此元素時會使用它最新的值,而非原先的值
  • 如果尚未被遍歷到的元素被刪除了,那它將不會被遍歷到

上述這種高併發 (concurrent) 的更動會導致程式碼非常難以閱讀,非常不建議使用 (除非有特殊的情境)。


ECMAScript

13.1

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 被呼叫時的 thisundefined ,驗證了上面所說的 - 我們無法指定 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()這類咩色還有一個很重要的地方,就是可以減少變數的宣告,不但可以提高閱讀性,還可以減少變數污染之類的問題。

最後,希望大家可以開心地使用各種咩色,體驗它帶給你的便利,祝大家歸剛沒煩惱。


參考資源


上一篇
Day 12 咩色用得好 - Array.prototype.pop
下一篇
Day14 咩色用得好 - Array.prototype.some (沒事沒事,有就好了)
系列文
咩色用得好,歸剛沒煩惱 - 從 ECMAScript 偷窺 JavaScript Array method30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
liugoldent
iT邦新手 5 級 ‧ 2023-08-11 15:00:54

特地來留一下reduce
以前新人看到reduce真的超就崩潰啦

但自從一次茅塞頓開之後
真的就是好用而已
要離職之前還故意留一堆reduce的code給後人(誤
印象中我出面試題還故意出這個,要考一下是不是真的懂

我要留言

立即登入留言