整篇會分成以下幾個部分:
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()
,我們將在下一篇介紹。
最後,希望大家可以開心地使用各種咩色,體驗它帶給你的便利,祝大家歸剛沒煩惱。