前端阿里面試—— 實現一個深拷貝


前言

深拷貝這個功能在開發中經常使用到,特別在對引用類型的數據進行操作時,一般會先深拷貝一份賦值給一個變量,然后在對其操作,防止影響到其它使用該數據的地方。

如何實現一個深拷貝,在面試中出現頻率一直居高不下。因為在實現一個深拷貝過程中,可以看出應聘者很多方面的能力。

本專欄將從青銅到王者來介紹怎么實現一個深拷貝,以及每個段位對應的能力。

青銅段位

JSON.parse(JSON.stringify(data)) 

這種寫法非常簡單,而且可以應對大部分的應用場景,但是它有很大缺陷的。如果你不知道它有那些缺陷,而且這種實現方法體現不出你任何能力,所以這種實現方法處於青銅段位。

  • 如果對象中存在循環引用的情況也無法正確實現深拷貝。
const a = { b: 1, } a.c = a; JSON.parse(JSON.stringify(a)); 
  • 如果 data 里面有時間對象,則JSON.stringify后再JSON.parse的結果,時間將只是字符串的形式。而不是時間對象。
const a = { b: new Date(1536627600000), } console.log(JSON.parse(JSON.stringify(a))) 
  • 如果 data 里有RegExp、Error對象,則序列化的結果將只得到空對象;
const a = { b: new RegExp(/\d/), c: new Error('錯誤') } console.log(JSON.parse(JSON.stringify(a))) 
  • 如果 data 里有函數,undefined,則序列化的結果會把函數置為undefined或丟失;
const a = { b: function (){ console.log(1) }, c:1, d:undefined } console.log(JSON.parse(JSON.stringify(a))) 
  • 如果 data 里有NaN、Infinity和-Infinity,則序列化的結果會變成null
const a = { b: NaN, c: 1.7976931348623157E+10308, d: -1.7976931348623157E+10308, } console.log(JSON.parse(JSON.stringify(a))) 

白銀段位

深拷貝的核心就是對引用類型的數據的拷貝處理。

function deepClone(target){ if(target !== null && typeof target === 'object'){ let result = {} for (let k in target){ if (target.hasOwnProperty(k)) { result[k] = deepClone(target[k]) } } return result; }else{ return target; } } 

以上代碼中,deepClone函數的參數 target 是要深拷貝的數據。

執行 target !== null && typeof target === 'object' 判斷 target 是不是引用類型。

若不是,直接返回 target

若是,創建一個變量 result 作為深拷貝的結果,遍歷 target,執行 deepClone(target[k]) 把 target 每個屬性的值深拷貝后賦值到深拷貝的結果對應的屬性 result[k] 上,遍歷完畢后返回 result

在執行 deepClone(target[k]) 中,又會對 target[k] 進行類型判斷,重復上述流程,形成了一個遞歸調用 deepClone 函數的過程。就可以層層遍歷要拷貝的數據,不管要拷貝的數據有多少子屬性,只要子屬性的值的類型是引用類型,就會調用 deepClone 函數將其深拷貝后賦值到深拷貝的結果對應的屬性上。

另外使用 for...in 循環遍歷對象的屬性時,其原型鏈上的所有屬性都將被訪問,如果只要只遍歷對象自身的屬性,而不遍歷繼承於原型鏈上的屬性,要使用 hasOwnProperty 方法過濾一下。

在這里可以向面試官展示你的三個編程能力。

  • 對原始類型和引用類型數據的判斷能力。
  • 對遞歸思維的應用的能力。
  • 深入理解for...in的用法。

黃金段位

白銀段位的代碼中只考慮到了引用類型的數據是對象的情況,漏了對引用類型的數據是數組的情況。

function deepClone(target){ if(target !== null && typeof target === 'object'){ let result = Object.prototype.toString.call(target) === "[object Array]" ? [] : {}; for (let k in target){ if (target.hasOwnProperty(k)) { result[k] = deepClone(target[k]) } } return result; }else{ return target; } } 

以上代碼中,只是額外增加對參數 target 是否是數組的判斷。執行 Object.prototype.toString.call(target) === "[object Array]" 判斷 target 是不是數組,若是數組,變量result 為 [],若不是數組,變量result 為 {}

在這里可以向面試官展示你的兩個編程能力。

  • 正確理解引用類型概念的能力。
  • 精確判斷數據類型的能力。

鉑金段位

假設要深拷貝以下數據 data

let data = { a: 1 }; data.f=data 

執行 deepClone(data),會發現控制台報錯,錯誤信息如下所示。

image
image

 

這是因為遞歸進入死循環導致棧內存溢出了。根本原因是 data 數據存在循環引用,即對象的屬性間接或直接的引用了自身。

function deepClone(target) { function clone(target, map) { if (target !== null && typeof target === 'object') { let result = Object.prototype.toString.call(target) === "[object Array]" ? [] : {}; if (map[target]) { return map[target]; } map[target] = result; for (let k in target) { if (target.hasOwnProperty(k)) { result[k] = deepClone(target[k]) } } return result; } else { return target; } } let map = {} const result = clone(target, map); map = null; return result } 

以上代碼中利用額外的變量 map 來存儲當前對象和拷貝對象的對應關系,當需要拷貝當前對象時,先去 map 中找,有沒有拷貝過這個對象,如果有的話直接返回,如果沒有的話繼續拷貝,這樣就巧妙化解的循環引用的問題。最后需要把變量 map 置為 null ,釋放內存,防止內存泄露。

在這里可以向面試官展示你的兩個編程能力。

  • 對循環引用的理解,如何解決循環引用引起的問題的能力。
  • 對內存泄露的認識和避免泄露的能力。

磚石段位

該段位要考慮性能問題了。在上面的代碼中,我們遍歷數組和對象都使用了 for...in 這種方式,實際上 for...in 在遍歷時效率是非常低的,故用效率比較高的 while 來遍歷。

function deepClone(target) { /** * 遍歷數據處理函數 * @array 要處理的數據 * @callback 回調函數,接收兩個參數 value 每一項的值 index 每一項的下標或者key。 */ function handleWhile(array, callback) { const length = array.length; let index = -1; while (++index < length) { callback(array[index], index) } } function clone(target, map) { if (target !== null && typeof target === 'object') { let result = Object.prototype.toString.call(target) === "[object Array]" ? [] : {}; if (map[target]) { return map[target]; } map[target] = result; const keys = Object.prototype.toString.call(target) === "[object Array]" ? undefined : Object.keys( target); function callback(value, key) { if (keys) { // 如果keys存在則說明value是一個對象的key,不存在則說明key就是數組的下標。 key = value; } result[key] = clone(target[key], map) } handleWhile(keys || target, callback) return result; } else { return target; } } let map = {} const result = clone(target, map); map = null; return result } 

用 while 遍歷的深拷貝記為 deepClone,把用 for ... in 遍歷的深拷貝記為 deepClone1。利用 console.time() 和 console.timeEnd() 來計算執行時間。

let arr = []; for (let i = 0; i < 1000000; i++) { arr.push(i) } let data = { a: arr }; console.time(); const result = deepClone(data); console.timeEnd(); console.time(); const result1 = deepClone1(data); console.timeEnd(); 

從上圖明顯可以看到用 while 遍歷的深拷貝的性能遠優於用 for ... in 遍歷的深拷貝。

在這里可以向面試官展示你的四個編程能力。

  • 具有優化代碼運行性能的能力。
  • 了解遍歷的效率的能力。
  • 了解 ++i 和 i++ 的區別。
  • 代碼抽象的能力。

星耀段位

在這個階段應該考慮代碼邏輯的嚴謹性。在上面段位的代碼雖然已經滿足平時開發的需求,但是還是有幾處邏輯不嚴謹的地方。

  • 判斷數據不是引用類型時就直接返回 target,但是原始類型中還有 Symbol 這一特殊類型的數據,因為其每個 Symbol 都是獨一無二,需要額外拷貝處理,不能直接返回。

  • 判斷數據是不是引用類型時不嚴謹,漏了 typeof target === function' 的判斷。

  • 只考慮了 Array、Object 兩種引用類型數據的處理,引用類型的數據還有Function 函數、Date 日期、RegExp 正則、Map 數據結構、Set 數據機構,其中 Map 、Set 屬於 ES6 的。

廢話不多說,直接貼上全部代碼,代碼中有注釋。

function deepClone(target) { // 獲取數據類型 function getType(target) { return Object.prototype.toString.call(target) } //判斷數據是不是引用類型 function isObject(target) { return target !== null && (typeof target === 'object' || typeof target === 'function'); } //處理不需要遍歷的應引用類型數據 function handleOherData(target) { const type = getType(target); switch (type) { case "[object Date]": return new Date(target) case "[object RegExp]": return cloneReg(target) case "[object Function]": return cloneFunction(target) } } //拷貝Symbol類型數據 function cloneSymbol(targe) { const a = String(targe); //把Symbol字符串化 const b = a.substring(7, a.length - 1); //取出Symbol()的參數 return Symbol(b); //用原先的Symbol()的參數創建一個新的Symbol } //拷貝正則類型數據 function cloneReg(target) { const reFlags = /\w*$/; const result = new target.constructor(target.source, reFlags.exec(target)); result.lastIndex = target.lastIndex; return result; } //拷貝函數 function cloneFunction(targe) { //匹配函數體的正則 const bodyReg = /(?<={)(.|\n)+(?=})/m; //匹配函數參數的正則 const paramReg = /(?<=\().+(?=\)\s+{)/; const targeString = targe.toString(); //利用prototype來區分下箭頭函數和普通函數,箭頭函數是沒有prototype的 if (targe.prototype) { //普通函數 const param = paramReg.exec(targeString); const body = bodyReg.exec(targeString); if (body) { if (param) { const paramArr = param[0].split(','); //使用 new Function 重新構造一個新的函數 return new Function(...paramArr, body[0]); } else { return new Function(body[0]); } } else { return null; } } else { //箭頭函數 //eval和函數字符串來重新生成一個箭頭函數 return eval(targeString); } } /** * 遍歷數據處理函數 * @array 要處理的數據 * @callback 回調函數,接收兩個參數 value 每一項的值 index 每一項的下標或者key。 */ function handleWhile(array, callback) { let index = -1; const length = array.length; while (++index < length) { callback(array[index], index); } } function clone(target, map) { if (isObject(target)) { let result = null; if (getType(target) === "[object Array]") { result = [] } else if (getType(target) === "[object Object]") { result = {} } else if (getType(target) === "[object Map]") { result = new Map(); } else if (getType(target) === "[object Set]") { result = new Set(); } //解決循環引用 if (map[target]) { return map[target]; } map[target] = result; if (getType(target) === "[object Map]") { target.forEach((value, key) => { result.set(key, clone(value, map)); }); return result; } else if (getType(target) === "[object Set]") { target.forEach(value => { result.add(clone(value, map)); }); return result; } else if (getType(target) === "[object Object]" || getType(target) === "[object Array]") { const keys = getType(target) === "[object Array]" ? undefined : Object.keys(target); function callback(value, key) { if (keys) { // 如果keys存在則說明value是一個對象的key,不存在則說明key就是數組的下標。 key = value } result[key] = clone(target[key], map) } handleWhile(keys || target, callback) } else { result = handleOherData(target) } return result; } else { if (getType(target) === "[object Symbol]") { return cloneSymbol(target) } else { return target; } } } let map = {} const result = clone(target, map); map = null; return result } 

在這里可以向面試官展示你的六個編程能力。

  • 代碼邏輯的嚴謹性。
  • 深入了解數據類型的能力。
  • JS Api 的熟練使用的能力。
  • 了解箭頭函數和普通函數的區別。
  • 熟練使用正則表達式的能力。
  • 模塊化開發的能力

王者段位

以上代碼中還有很多數據類型的拷貝,沒有實現,有興趣的話可以在評論中實現一下,王者屬於你哦!

總結

綜上所述,面試官叫你實現一個深拷貝,其實是要考察你各方面的能力。例如

  • 白銀段位
    • 對原始類型和引用類型數據的判斷能力。
    • 對遞歸思維的應用的能力。
  • 黃金段位
    • 正確理解引用類型概念的能力。
    • 精確判斷數據類型的能力。
  • 鉑金段位
    • 對循環引用的理解,如何解決循環引用引起的問題的能力。
    • 對內存泄露的認識和避免泄露的能力。
  • 磚石段位
    • 具有優化代碼運行性能的能力。
    • 了解遍歷的效率的能力。
    • 了解 ++i 和 i++ 的區別。
    • 代碼抽象的能力。
  • 星耀段位
    • 代碼邏輯的嚴謹性。
    • 深入了解數據類型的能力。
    • JS Api 的熟練使用的能力。
    • 了解箭頭函數和普通函數的區別。
    • 熟練使用正則表達式的能力。
    • 模塊化開發的能力

所以不要去死記硬背一些手寫代碼的面試題,最好自己動手寫一下,看看自己達到那個段位了。

最后

對於大廠面試,我最后想要強調的一點就是心態真的很重要,是決定你在面試過程中發揮的關鍵,若不能正常發揮,很可能就因為一個小失誤與offer失之交臂,所以一定要重視起來。另外提醒一點,充分復習,是消除你緊張的心理狀態的關鍵,但你復習充分了,自然面試過程中就要有底氣得多。

我平時一直有整理面試題的習慣,有隨時跳出舒適圈的准備,不知不覺整理了229頁了,在這里分享給大家,有需要的點擊這里免費領取題目+解析PDF

篇幅有限,僅展示部分內容

如果你需要這份完整版的面試題+解析,【點擊我】就可以了。

希望大家明年的金三銀四面試順利,拿下自己心儀的offer!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM