iT邦幫忙

2022 iThome 鐵人賽

DAY 18
0

Abstract

整篇會分成以下幾個部分:

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

flat() 這個 method 的全寫應該是 Array.prototype.flat,有興趣可以看 Day 2 的介紹,這邊會直接使用 flat() 作為替代。

最後會透過分析 ECMAScript 來驗證是否有吻合,如果覺得 ECMAScript 有點艱澀難懂,我們在 Day 4 、Day 5 有介紹其相關術語可以幫助閱讀。

flat() 的 ECMAScript 演算法略為繁雜,但挺值得一讀,大家有空可以往下看 ECMAScript 的介紹!


使用時機

當你想要把巢狀陣列中的子陣列展開時,例如以下:

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

你可以給 flat() 一個參數決定展開至巢狀陣列的第幾層。


語法

flat()

flat(depth)

參數

只接受一個可選的 depth 參數。

depth (optional)

depth 可以決定要展開的子陣列層數,預設為 1,也就是第一層的子陣列會被展開。

depth 可以是負數,但會被轉換成 0,也可以是小數,但會被轉換成整數。

Return Value

回傳一個將子陣列展開的新陣列。

Mutability

不會變動到原陣列。


說明

flat() 可以快速地將陣列展開, 還可以透過 depth 決定展開的層數,並在不變動原陣列的情況下,回傳一個展開後的新陣列。

flat() 會直接忽略稀疏陣列 (sparse array) 的 empty slot,這意味著被展開的子陣列中的 empty slot 皆會被拿掉,例如以下:

const array = [1, , [3, , 3], 4, 5]

const flattenArray = array.flat()

console.log(flattenArray)
// [1, 3, 3, 4, 5]

範例

Example 1 -基礎用法

const array = [1, [2, 2], 3, [4, [4, 4]], 5]

const flattenOneLevel = array.flat()
console.log( flattenOneLevel)
// [1, 2, 2, 3, 4, [4, 4], 5]

const flattenTwoLevel = array.flat(2)
console.log(flattenTwoLevel)
// [1, 2, 2, 3, 4, 4, 4, 5]

沒有給予 depth 時,預設為 1 ,所有第 1 層的子陣列會被展開。

Example 2 - depth 為小數或負數時

const array = [1, [2, 2], 3, [4, [4, 4]], 5]

const flattenArray1 = array.flat(0.6)
console.log(flattenArray1)
// [1, [2, 2], 3, [4, [4, 4]], 5]
// 0.6 -> 0

const flattenArray2 = array.flat(1.3)
console.log(flattenArray2)
// [1, 2, 2, 3, 4, [4, 4], 5]
// 1.3 -> 1

const flattenArray3 = array.flat(-2)
console.log(flattenArray3)
// [1, [2, 2], 3, [4, [4, 4]], 5]
// -2 -> 0

const flattenArray4 = array.flat(-0.8)
console.log(flattenArray4)
// [1, [2, 2], 3, [4, [4, 4]], 5]
// -0.8 -> 0

depth 為小數時會被轉成整數,為負數時會被轉成 0。

Example 3 - flat() 的替代方法

const array = [1, 2, [3, 3], 4]

const flattenArray1 = array.reduce((flattenArray, element) => flattenArray.concat(element), [])
console.log(flattenArray1)
// [1, 2, 3, 3, 4]

const flattenArray2  = array.reduce((flattenArray, element) => {
	if (!Array.isArray(element)) return [...flattenArray, element]
	return [...flattenArray, ...element]
}, [])
console.log(flattenArray2)
// [1, 2, 3, 3, 4]

上述的 2 個方法都只能展開第 1 層的子陣列,如果想要展開全部的子陣列可以考慮使用遞迴的方式。


注意事項

depth 使用小數時要注意轉成整數的方式,一律會無條件捨去。

除了 flat() 以外,還有不少方式可以展開陣列,例如範例的 Example 3,


ECMAScript

由於 flat() 演算法的本體是在 FlattenIntoArray 這個抽象操作上,因此這邊會分成 2 個部分介紹:

Array.prototype.flat([depth])

18.1

flat() 的演算法並沒有要求呼叫它的必須是一個陣列,可以從步驟 1 得知,為了方便解釋,這邊一律使用陣列來說明。

步驟 3、4 可以看到預設的 depth 為 1,如果有提供 depth 則會將其轉換成整數,如果小於 0,則設為 0,這驗證了前面的參數說明。

步驟 5 利用了原陣列創造一個長度為 0 的新陣列,並指派給 A

步驟 6 則執行了陣列的展開,使用的是 FlattenIntoArray 這個抽象操作,下面會繼續介紹。

最後將這個展開後的新陣列回傳出來,也驗證了 flat() 不會改動到原陣列。

我們來驗證一下它是否可以使用在非陣列的物件上:

18.2

FlattenIntoArray ( target, source, sourceLen, start, depth [ , mapperFunction [ , thisArg ] ] )

18.3

這個抽象操作有多達 7 個參數,其中 2 個為可選的參數,我們來慢慢拆解。

步驟 1 表明了如果有帶入 mapperFunction,那 thisArg 也會存在,且 depth 會是 1,但實際上,flat() 的演算法在呼叫 FlattenIntoArray 時並沒有帶入 mapperFunction,因此我們會略過這個部分。

步驟 2 的 targetIndex 非常重要,也是最容易搞混的地方,我們先記住這個 targetIndex 就是新陣列每次要新增元素的索引 (位置),它會從 0 開始。

步驟 4 便開始遍歷原陣列執行迴圈:

  • 每次都會查看當前的屬性是否存在,因為我們是以使用陣列為前提,原則上都會有這個索引屬性存在,因此我們就跳過這項檢查。
  • 依照目前遍歷的索引取出相對應的元素 element,取出 element 的目的有 2 個:
    • mapperFunction 操作使用,但 flat() 不會帶入 mapperFunction,因此我們略過。
    • 檢查這個 element 是否為一個陣列,如果是就必須在做額外的操作。
  • 接著如果 depth 大於 0 表示要展開子陣列,所以會再去查看這個 element 是否為一個陣列。
  • 如果 element 為一陣列,表示它將會被展開,那它要怎麼被展開呢?也就是再呼叫一次 FlattenIntoArray(),代表演算法使用的是遞迴 (recursion) 的方式來不斷展開其中的子陣列,而既然使用了遞迴,意味著會有一個終止條件 (base case) 的存在,也就是當 depth ≤ 0 時。
  • 每次重新呼叫 FlattenIntoArray() 時都會帶入同一個新陣列 - target、當前的子陣列 - element、子陣列的長度 - elementLength 、新陣列要添加元素的位置 - targetIndex,還有一個會不斷減少的 newDepth
  • 因為每次的遞迴都會不斷地減少欲展開的層數 depth,所以當這個子陣列展開至指定的層數時便會停止遞迴。
  • 如果當前的元素不是陣列時,則直接將當前元素添加至新陣列,並讓 targetInex + 1。

這邊要非常注意的是 targetIndex 這個變數,它是用來記錄每次新元素要添加至新陣列的位置,所以每次添加新元素後或 FlattenIntoArray() 被呼叫後都會回傳新的 targetIndex 來告訴新陣列下次要添加元素的起始位置。

等到原始陣列被遍歷完畢、新陣列添加元素完畢後便停止迴圈,也表示陣列被展開完畢,最後回傳一個不會使用到的 targetIndex

出現 ? 的地方代表有可能會丟出錯誤,所以整個演算法有 7 處有機會丟出錯誤,例如步驟 4-c-vi-2 的 CreateDataPropertyOrThrow(),當你新增一個屬性失敗,便會丟出一個 TypeError

如果出現 ! ,則代表這個 abstract operation 絕對不會丟出錯誤,例如步驟 4-a 的 ToString() 它會在參數是一個 Symbol 時丟出一個 TypeError,但我們確定丟進去的是一個 Number (sourceIndex),因此不會有丟出錯誤的可能。


結論

我個人認為 flat() 很強大, 它展開陣列快速、方便又直覺,而它還有功能更強大的升級版 flatMap(),我們將在下一篇介紹。

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


參考資源


上一篇
Day 17 咩色用得好 - Array.prototype.includes
下一篇
Day 19 咩色用得好 - Array.prototype.slice
系列文
咩色用得好,歸剛沒煩惱 - 從 ECMAScript 偷窺 JavaScript Array method30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言