一文搞定陣列扁平化(超全面的陣列拉平方案及實現)
前言
面試手寫程式碼在大廠面試中非常常見,秋招中面試小米就手寫了一道flat實現的程式碼題,當時通過遞迴方式實現了陣列扁平化邏輯,但沒有考慮多種實現方案及其邊界條件(主要是對所涉及到高階函式的知識點不夠熟練,也沒有考慮空位處理),現在從頭梳理一下,並儘可能全面地總結陣列扁平化的實現方案。
陣列扁平化
陣列扁平化即將一個巢狀多層的陣列array(巢狀可以是任意層數)轉換為只有一層的陣列,如將陣列[1,[2,[3,[4,5]]]]轉換為[1,2,3,4,5]。
最直接的陣列扁平化方案是使用Array.prototype.flat()方法(相容性差),其次是通過遍歷陣列元素遞迴實現每一層的陣列拉平。
00x1 Array.prototype.flat()
按照一個可指定的深度遞迴遍歷陣列,並將所有元素與遍歷到的子陣列中的元素合併為一個新陣列返回,對原資料沒有影響。
語法:var newArray = arr.flat([depth])
說明:
- depth為指定要提取巢狀陣列的結構深度,預設值為1。
- 引數depth值 <=0 時返回原陣列;
- 引數depth為Infinity 關鍵字時,無論多少層巢狀,都會轉為一維陣列,
- flat()方法會移除陣列中的空項,即原陣列有空位,會跳過這個空位。
程式碼示例:
var arr1 = [1, 2, [3, 4]]; arr1.flat(); // [1, 2, 3, 4] var arr2 = [1, 2, [3, 4, [5, 6]]]; arr2.flat(); // [1, 2, 3, 4, [5, 6]] var arr3 = [1, 2, [3, 4, [5, 6]]]; arr3.flat(2); // [1, 2, 3, 4, 5, 6] //使用 Infinity,可展開任意深度的巢狀陣列 var arr4 = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]]; arr4.flat(Infinity); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] var arr5 = [1, 2, 3, 4, [1, 2, 3, [1, 2, 3, [1, 2, 3]]], 5, ["string", { type: "物件" }]]; arr5.flat(Infinity); // [1, 2, 3, 4, 1, 2, 3, 1, 2, 3, 1, 2, 3, 5, "string", { type: "物件" }]; // 移除陣列中的空項 var arr6 = [1, 2, , 4, 5]; arr6.flat(); // [1, 2, 4, 5]
陣列扁平化flat函式封裝實現方案
實現思路
首先遍歷獲取陣列的每一個元素,其次判斷該元素型別是否為陣列,最後將陣列型別的元素展開一層。同時遞迴遍歷獲取該陣列的每個元素進行拉平處理。
遍歷陣列方案
- for迴圈
- for...of
- for...in
- entries()
- keys()
- values()
- forEach()
- map()
- reduce()
const arr = [1, 2, 3, 4, [1, 2, 3, [1, 2, 3, [1, 2, 3]]], 5, ["string", { type: "物件" }]]; // 本文只列舉常用的幾種陣列遍歷方法 // for 迴圈 for (let i = 0; i < arr.length; i++) { console.log(arr[i]); } // for...of for (let value of arr) { console.log(value); } // for...in for (let i in arr) { console.log(arr[i]); } // forEach 迴圈 arr.forEach(value => { console.log(value); }); // entries() for (let [index, value] of arr.entries()) { console.log(value); } // keys() for (let index of arr.keys()) { console.log(arr[index]); } // values() for (let value of arr.values()) { console.log(value); } // reduce() arr.reduce((pre, cur) => { console.log(cur); }, []); // map() arr.map(value => console.log(value));
判斷陣列元素是否為陣列
- instanceof
- constructor
- Object.prototype.toString
- isArray
const arr = [1, 2, 3, 4, [1, 2, 3, [1, 2, 3, [1, 2, 3]]], 5, ["string", { type: "物件" }]]; arr instanceof Array // true arr.constructor === Array // true Object.prototype.toString.call(arr) === '[object Array]' // true Array.isArray(arr) // true
注:
- instanceof 操作符是假定只有一種全域性環境,如果網頁中包含多個框架,多個全域性環境,如果你從一個框架向另一個框架傳入一個數組,那麼傳入的陣列與在第二個框架中原生建立的陣列分別具有各自不同的建構函式。(所以在這種情況下會不準確)
- typeof 操作符對陣列取型別將返回 object
- constructor可以被重寫,不能確保一定是陣列
const str = 'abc'; str.constructor = Array; str.constructor === Array // true
陣列元素展開一層方案
- 擴充套件運算子 + concat
- concat +appl
- toString + split
不推薦使用toString+split方法,操作字串是很危險的,陣列中元素都是數字時可行。
const arr = [1, 2, 3, 4, [1, 2, 3, [1, 2, 3, [1, 2, 3]]], 5, ["string", { type: "物件" }]]; // 擴充套件運算子 + concat [].concat(...arr) // [1, 2, 3, 4, 1, 2, 3, [1, 2, 3, [1, 2, 3]], 5, "string", { type: "物件" }]; // concat + apply [].concat.apply([], arr); // [1, 2, 3, 4, 1, 2, 3, [1, 2, 3, [1, 2, 3]], 5, "string", { name: "物件" }]; // toString + split const arr2 =[1, 2, 3, 4, [1, 2, 3, [1, 2, 3, [1, 2, 3]]]] arr2.toString().split(',').map(v=>parseInt(v)) // [1, 2, 3, 4, 1, 2, 3, 1, 2, 3, 1, 2, 3]
00x1 手寫一個最簡單的flat函式實現
這裡使用ES6語法中的箭頭函式定義函式,注意箭頭函式沒有arguments,caller,callee,同時要區分於ES5使用function的兩種函式宣告定義方式。
const arr = [1, 2, 3, 4, [1, 2, 3, [1, 2, 3, [1, 2, 3]]], 5, ["string", { type: "物件" }]]; const flat = (arr) => { let arrResult = [] for(let i=0, len=arr.length; i<len; i++){ if(Array.isArray(arr[i])){ arrResult.push(...flat(arr[i])) // arrResult = arrResult.concat(flat(arr[i])) }else{ arrResult.push(arr[i]) } } return arrResult; } flat(arr) // [1, 2, 3, 4, 1, 2, 3, 1, 2, 3, 1, 2, 3, 5, "string", { type: "物件" }]
迴圈部分同理可用for...of / for...in 來實現。
OK,現在你已經具備了基本的手撕程式碼能力,但面試官常常希望你能掌握各種高階函式方法的應用。接下來繼續列舉實現flat的幾種方案。
00x2 用map/forEach實現flat函式
仍然是遍歷+迴圈的原理,這裡迴圈用map/forEach實現。
const arr = [1, 2, 3, 4, [1, 2, 3, [1, 2, 3, [1, 2, 3]]], 5, ["string", { type: "物件" }]]; const flat = (arr) => { let arrResult = [] arr.map(item => { if(Array.isArray(item)){ arrResult.push(...flat(item)) // arrResult = arrResult.concat(flat(item)) }else{ arrResult.push(item) } }) return arrResult; } flat(arr) // [1, 2, 3, 4, 1, 2, 3, 1, 2, 3, 1, 2, 3, 5, "string", { type: "物件" }]
00x3 歸併方法:用reduce實現flat函式
我們用reduce函式進行遍歷,把prev的初值賦值為[],如果當前的值是陣列的話,那麼我們就遞迴遍歷它的孩子,如果當前的值不是陣列,那麼我們就把它拼接進數組裡。
const arr = [1, 2, 3, 4, [1, 2, 3, [1, 2, 3, [1, 2, 3]]], 5, ["string", { type: "物件" }]]; function flat(arr) { return arr.reduce((prev, cur)=>{ return prev.concat(Array.isArray(cur)?flat(cur):cur); }, []) } flat(arr) // [1, 2, 3, 4, 1, 2, 3, 1, 2, 3, 1, 2, 3, 5, "string", { type: "物件" }]
00x4 用Generator實現flat函式
function* flat(arr, num) { if (num === undefined) num = 1; for (const item of arr) { if (Array.isArray(item) && num > 0) { // num > 0 yield* flat(item, num - 1); } else { yield item; } } } const arr = [1, 2, 3, 4, [1, 2, 3, [1, 2, 3, [1, 2, 3]]], 5, ["string", { type: "物件" }]] // 呼叫 Generator 函式後,該函式並不執行,返回的也不是函式執行結果,而是一個指向內部狀態的指標物件。 // 也就是遍歷器物件(Iterator Object)。所以我們要用一次擴充套件運算子得到結果 [...flat(arr, Infinity)] // [1, 2, 3, 4, 1, 2, 3, 1, 2, 3, 1, 2, 3, 5, "string", { type: "物件" }]
00x5 在原型鏈上重寫 flat 函式
Array.prototype.fakeFlat = function(num = 1) { if (!Number(num) || Number(num) < 0) { return this; } let arr = this.concat(); // 獲得呼叫 fakeFlat 函式的陣列 while (num > 0) { if (arr.some(x => Array.isArray(x))) { arr = [].concat.apply([], arr); // 陣列中還有陣列元素的話並且 num > 0,繼續展開一層陣列 } else { break; // 陣列中沒有陣列元素並且不管 num 是否依舊大於 0,停止迴圈。 } num--; } return arr; }; const arr = [1, 2, 3, 4, [1, 2, 3, [1, 2, 3, [1, 2, 3]]], 5, ["type", { name: "物件" }]] arr.fakeFlat(Infinity) // [1, 2, 3, 4, 1, 2, 3, 1, 2, 3, 1, 2, 3, 5, "string", { type: "物件" }]
00x6 使用棧的思想實現 flat 函式
// 棧思想 function flat(arr) { const result = []; const stack = [].concat(arr); // 將陣列元素拷貝至棧,直接賦值會改變原陣列 //如果棧不為空,則迴圈遍歷 while (stack.length !== 0) { const val = stack.pop(); if (Array.isArray(val)) { stack.push(...val); //如果是陣列再次入棧,並且展開了一層 } else { result.unshift(val); //如果不是陣列就將其取出來放入結果陣列中 } } return result; } const arr = [1, 2, 3, 4, [1, 2, 3, [1, 2, 3, [1, 2, 3]]], 5, ["string", { type: "物件" }]] flat(arr) // [1, 2, 3, 4, 1, 2, 3, 1, 2, 3, 1, 2, 3, 5, "string", { type: "物件" }]
00x7 通過傳入整數引數控制“拉平”層數
// reduce + 遞迴 function flat(arr, num = 1) { return num > 0 ? arr.reduce( (pre, cur) => pre.concat(Array.isArray(cur) ? flat(cur, num - 1) : cur), [] ) : arr.slice(); } const arr = [1, 2, 3, 4, [1, 2, 3, [1, 2, 3, [1, 2, 3]]], 5, ["string", { type: "物件" }]] flat(arr, Infinity); // [1, 2, 3, 4, 1, 2, 3, 1, 2, 3, 1, 2, 3, 5, "string", { type: "物件" }]
00x8 陣列空位的處理
flat 函式執行是會跳過空位的。
ES5 對空位的處理,大多數情況下會忽略空位。
- forEach(), filter(), reduce(), every() 和 some() 都會跳過空位。
- map() 會跳過空位,但會保留這個值。
- join() 和 toString() 會將空位視為 undefined,而 undefined 和 null 會被處理成空字串。
ES6 明確將空位轉為 undefined。
- entries()、keys()、values()、find()和 findIndex() 會將空位處理成 undefined。
- for...of 迴圈會遍歷空位。
- fill() 會將空位視為正常的陣列位置。
- copyWithin() 會連空位一起拷貝。
- 擴充套件運算子(...)也會將空位轉為 undefined。、
- Array.from 方法會將陣列的空位,轉為 undefined。
00x1 for...of 迴圈遍歷實現flat函式
const arr1 = [1, 2, 3, , , 4, [1, 2, 3, [1, 2, 3, [1, 2, 3]]], 5, ["string", { type: "物件" }]]; const flat = (arr) => { let arrResult = [] for(let item of arr){ if(Array.isArray(item)){ arrResult.push(...flat(item)) // arrResult = arrResult.concat(flat(arr[i])) }else{ arrResult.push(item) } } return arrResult; } flat(arr1) // [1, 2, 3, undefined, undefined, 4, 1, 2, 3, 1, 2, 3, 1, 2, 3, 5, "string", { type: "物件" }]
## 總結
現在的前端面試中,大廠面試官基本都會考察手撕程式碼的能力,不僅要能答得上來實現陣列扁平化的幾種方案,也不僅是要能手寫實現,還要能理解,能講清楚其中包涵的詳細知識點及程式碼的邊界情況,能在基礎版本上再寫出一個更完美的版本。
而我們在寫程式碼的過程中,也要養成這樣的習慣,多問問自己還有沒有別的替代實現方案,還能不能進一步優化,才能寫出優美漂亮的程式碼,程式設計能力自然而然也就提高