前端阿里面試—— 實現一個深拷貝
前言
深拷貝這個功能在開發中經常使用到,特別在對引用型別的資料進行操作時,一般會先深拷貝一份賦值給一個變數,然後在對其操作,防止影響到其它使用該資料的地方。
如何實現一個深拷貝,在面試中出現頻率一直居高不下。因為在實現一個深拷貝過程中,可以看出應聘者很多方面的能力。
本專欄將從青銅到王者來介紹怎麼實現一個深拷貝,以及每個段位對應的能力。
青銅段位
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
這是因為遞迴進入死迴圈導致棧記憶體溢位了。根本原因是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!