整篇會分成以下幾個部分:
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,也可以是小數,但會被轉換成整數。
回傳一個將子陣列展開的新陣列。
不會變動到原陣列。
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]
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 層的子陣列會被展開。
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。
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,
Array.prototype.includes([depth])
由於 flat() 演算法的本體是在 FlattenIntoArray 這個抽象操作上,因此這邊會分成 2 個部分介紹:
Array.prototype.flat([depth])
flat() 的演算法並沒有要求呼叫它的必須是一個陣列,可以從步驟 1 得知,為了方便解釋,這邊一律使用陣列來說明。
步驟 3、4 可以看到預設的 depth 為 1,如果有提供 depth 則會將其轉換成整數,如果小於 0,則設為 0,這驗證了前面的參數說明。
步驟 5 利用了原陣列創造一個長度為 0 的新陣列,並指派給 A。
步驟 6 則執行了陣列的展開,使用的是 FlattenIntoArray 這個抽象操作,下面會繼續介紹。
最後將這個展開後的新陣列回傳出來,也驗證了 flat() 不會改動到原陣列。
我們來驗證一下它是否可以使用在非陣列的物件上:

FlattenIntoArray ( target, source, sourceLen, start, depth [ , mapperFunction [ , thisArg ] ] )
這個抽象操作有多達 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(),我們將在下一篇介紹。
最後,希望大家可以開心地使用各種咩色,體驗它帶給你的便利,祝大家歸剛沒煩惱。