前言
面試手寫代碼在大廠面試中非常常見,秋招中面試小米就手寫了一道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: "對象" }]
## 總結
現在的前端面試中,大廠面試官基本都會考察手撕代碼的能力,不僅要能答得上來實現數組扁平化的幾種方案,也不僅是要能手寫實現,還要能理解,能講清楚其中包涵的詳細知識點及代碼的邊界情況,能在基礎版本上再寫出一個更完美的版本。
而我們在寫代碼的過程中,也要養成這樣的習慣,多問問自己還有沒有別的替代實現方案,還能不能進一步優化,才能寫出優美漂亮的代碼,編程能力自然而然也就提高啦!