前端面試的底氣之實(shí)現(xiàn)一個(gè)深拷貝
前言
深拷貝這個(gè)功能在開發(fā)中經(jīng)常使用到,特別在對(duì)引用類型的數(shù)據(jù)進(jìn)行操作時(shí),一般會(huì)先深拷貝一份賦值給一個(gè)變量,然后在對(duì)其操作,防止影響到其它使用該數(shù)據(jù)的地方。
如何實(shí)現(xiàn)一個(gè)深拷貝,在面試中出現(xiàn)頻率一直居高不下。因?yàn)樵趯?shí)現(xiàn)一個(gè)深拷貝過程中,可以看出應(yīng)聘者很多方面的能力。
本專欄將從青銅到王者來介紹怎么實(shí)現(xiàn)一個(gè)深拷貝,以及每個(gè)段位對(duì)應(yīng)的能力。
青銅段位
JSON.parse(JSON.stringify(data))
這種寫法非常簡(jiǎn)單,而且可以應(yīng)對(duì)大部分的應(yīng)用場(chǎng)景,但是它有很大缺陷的。如果你不知道它有那些缺陷,而且這種實(shí)現(xiàn)方法體現(xiàn)不出你任何能力,所以這種實(shí)現(xiàn)方法處于青銅段位。
- 如果對(duì)象中存在循環(huán)引用的情況也無法正確實(shí)現(xiàn)深拷貝。
const a = {
b: 1,
}
a.c = a;
JSON.parse(JSON.stringify(a));
- 如果 data 里面有時(shí)間對(duì)象,則
JSON.stringify后再JSON.parse的結(jié)果,時(shí)間將只是字符串的形式。而不是時(shí)間對(duì)象。
const a = {
b: new Date(1536627600000),
}
console.log(JSON.parse(JSON.stringify(a)))
- 如果 data 里有RegExp、Error對(duì)象,則序列化的結(jié)果將只得到空對(duì)象;
const a = {
b: new RegExp(/\d/),
c: new Error('錯(cuò)誤')
}
console.log(JSON.parse(JSON.stringify(a)))
- 如果 data 里有函數(shù),undefined,則序列化的結(jié)果會(huì)把函數(shù)置為undefined或丟失;
const a = {
b: function (){
console.log(1)
},
c:1,
d:undefined
}
console.log(JSON.parse(JSON.stringify(a)))
- 如果 data 里有NaN、Infinity和-Infinity,則序列化的結(jié)果會(huì)變成null
const a = {
b: NaN,
c: 1.7976931348623157E+10308,
d: -1.7976931348623157E+10308,
}
console.log(JSON.parse(JSON.stringify(a)))
白銀段位
深拷貝的核心就是對(duì)引用類型的數(shù)據(jù)的拷貝處理。
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函數(shù)的參數(shù) target 是要深拷貝的數(shù)據(jù)。
執(zhí)行 target !== null && typeof target === 'object' 判斷 target 是不是引用類型。
若不是,直接返回 target。
若是,創(chuàng)建一個(gè)變量 result 作為深拷貝的結(jié)果,遍歷 target,執(zhí)行 deepClone(target[k]) 把 target 每個(gè)屬性的值深拷貝后賦值到深拷貝的結(jié)果對(duì)應(yīng)的屬性 result[k] 上,遍歷完畢后返回 result。
在執(zhí)行 deepClone(target[k]) 中,又會(huì)對(duì) target[k] 進(jìn)行類型判斷,重復(fù)上述流程,形成了一個(gè)遞歸調(diào)用 deepClone 函數(shù)的過程。就可以層層遍歷要拷貝的數(shù)據(jù),不管要拷貝的數(shù)據(jù)有多少子屬性,只要子屬性的值的類型是引用類型,就會(huì)調(diào)用 deepClone 函數(shù)將其深拷貝后賦值到深拷貝的結(jié)果對(duì)應(yīng)的屬性上。
另外使用 for...in 循環(huán)遍歷對(duì)象的屬性時(shí),其原型鏈上的所有屬性都將被訪問,如果只要只遍歷對(duì)象自身的屬性,而不遍歷繼承于原型鏈上的屬性,要使用 hasOwnProperty 方法過濾一下。
在這里可以向面試官展示你的三個(gè)編程能力。
- 對(duì)原始類型和引用類型數(shù)據(jù)的判斷能力。
- 對(duì)遞歸思維的應(yīng)用的能力。
- 深入理解for...in的用法。
黃金段位
白銀段位的代碼中只考慮到了引用類型的數(shù)據(jù)是對(duì)象的情況,漏了對(duì)引用類型的數(shù)據(jù)是數(shù)組的情況。
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;
}
}以上代碼中,只是額外增加對(duì)參數(shù) target 是否是數(shù)組的判斷。執(zhí)行 Object.prototype.toString.call(target) === "[object Array]" 判斷 target 是不是數(shù)組,若是數(shù)組,變量result 為 [],若不是數(shù)組,變量result 為 {}。
在這里可以向面試官展示你的兩個(gè)編程能力。
- 正確理解引用類型概念的能力。
- 精確判斷數(shù)據(jù)類型的能力。
鉑金段位
假設(shè)要深拷貝以下數(shù)據(jù) data
let data = {
a: 1
};
data.f=data執(zhí)行 deepClone(data),會(huì)發(fā)現(xiàn)控制臺(tái)報(bào)錯(cuò),錯(cuò)誤信息如下所示。

這是因?yàn)檫f歸進(jìn)入死循環(huán)導(dǎo)致棧內(nèi)存溢出了。根本原因是 data 數(shù)據(jù)存在循環(huán)引用,即對(duì)象的屬性間接或直接的引用了自身。
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] = clone(target[k],map)
}
}
return result;
} else {
return target;
}
}
let map = new Map();
const result = clone(target, map);
map.clear();
map = null;
return result
}以上代碼中利用額外的變量 map 來存儲(chǔ)當(dāng)前對(duì)象和拷貝對(duì)象的對(duì)應(yīng)關(guān)系,當(dāng)需要拷貝當(dāng)前對(duì)象時(shí),先去 map 中找,有沒有拷貝過這個(gè)對(duì)象,如果有的話直接返回,如果沒有的話繼續(xù)拷貝,這樣就巧妙化解的循環(huán)引用的問題。
之所以采用 ES6 中 Map 數(shù)據(jù)結(jié)構(gòu),是因?yàn)橄鄬?duì)于普通的 Object 結(jié)構(gòu),其鍵值不限于字符串,各種類型的值(包括對(duì)象)都可以當(dāng)作鍵。換句話來說。Object 結(jié)構(gòu)提供了“字符串——值”的對(duì)應(yīng),Map 結(jié)構(gòu)提供了“值——值” 的對(duì)應(yīng)。若采用 Object 結(jié)構(gòu),當(dāng) target 不是字符串時(shí),那么其鍵值全部都是 [Object Object],會(huì)引起混亂。
最后需要執(zhí)行 map.clear();map = null; ,釋放內(nèi)存,防止內(nèi)存泄露。
在這里可以向面試官展示你的三個(gè)編程能力。
- 對(duì)循環(huán)引用的理解,如何解決循環(huán)引用引起的問題的能力。
- 熟悉 ES6 中 Map 數(shù)據(jù)結(jié)構(gòu)的概念及應(yīng)用
- 對(duì)內(nèi)存泄露的認(rèn)識(shí)和避免泄露的能力。
磚石段位
該段位要考慮性能問題了。上面用 Map 來解決循環(huán)引用的問題。但是最后一定要執(zhí)行 map.clear();map = null; ,釋放內(nèi)存,防止內(nèi)存泄露。
也可以使用 WeakMap 來解決循環(huán)引用。WeakMap 數(shù)據(jù)結(jié)構(gòu)和 Map 數(shù)據(jù)結(jié)構(gòu),有兩點(diǎn)區(qū)別:
WeakMap 只接受對(duì)象作為鍵名( null 除外),不接受其他類型的值作為鍵名。但是 target 正好符合要求故不影響。
WeakMap 的鍵名所指向的對(duì)象不計(jì)入垃圾回收機(jī)制。這里用下面的例子來解釋。
let map = new WeakMap();
let obj = {a : 1}
map.set(obj , 1);現(xiàn)代瀏覽器GC回收策略,采用計(jì)算引用次來回收,就是某個(gè)對(duì)象被引用的次數(shù)為 1,因?yàn)?map 中的鍵名 obj 和對(duì)象 obj 之間的引用是弱引用,也就是不計(jì)入引用次數(shù),對(duì)象 obj 被引用的次數(shù)還是 1。 當(dāng)執(zhí)行 obj = null后,執(zhí)行GC回收時(shí),對(duì)象 obj 被引用的次數(shù)變成 0 ,故會(huì)把 obj 所占的內(nèi)存釋放掉。
但是如果 map 是 Map 數(shù)據(jù)結(jié)構(gòu),因?yàn)?Map 的鍵名所引用的對(duì)象是強(qiáng)引用,就算執(zhí)行 obj = null 后,再執(zhí)行GC回收,Map 的鍵名 obj 對(duì)所引用的對(duì)象 obj 還是有引用,那么obj 被引用的次數(shù)還是 1,故 obj 所占的內(nèi)存釋放不掉。
所以使用 WeakMap 可以防止忘記把 map 置為 null 導(dǎo)致的內(nèi)存泄漏。然而 WeakMap 這種弱引用,僅僅只是為了防止內(nèi)存泄漏嗎?其還有個(gè)特性,當(dāng)執(zhí)行 obj = null,相當(dāng)對(duì)象 obj 被GC回收了,但是執(zhí)行 map.get(obj) 得到的值還是 1。我們可以利用這個(gè)特性來做一下性能優(yōu)化。
在深拷貝的代碼中。是使用拷貝的對(duì)象作為鍵名的,設(shè)想一下,當(dāng)拷貝的對(duì)象非常龐大時(shí),導(dǎo)致 map 會(huì)占用很大的內(nèi)存。
如果 map 使用 Map 數(shù)據(jù)結(jié)構(gòu),因?yàn)?Map 的鍵名所引用的對(duì)象是強(qiáng)引用,所以在瀏覽器定時(shí)執(zhí)行GC回收時(shí),除非手動(dòng)清除 Map 這個(gè)鍵值對(duì),才能把這個(gè)拷貝對(duì)象所占用的內(nèi)存釋放掉。
如果 map 使用 WeakMap 數(shù)據(jù)結(jié)構(gòu),因?yàn)?WeakMap 的鍵名所引用的對(duì)象是弱引用,所以在瀏覽器會(huì)定時(shí)執(zhí)行GC回收,會(huì)直接把這個(gè)拷貝對(duì)象所占用的內(nèi)存釋放掉,那么這樣是不是對(duì)內(nèi)存占用減少,間接優(yōu)化了代碼運(yùn)行性能。
此外在上面的代碼中,我們遍歷數(shù)組和對(duì)象都使用了 for...in 這種方式,實(shí)際上 for...in 在遍歷時(shí)效率是非常低的,故用效率比較高的 while 來遍歷。
function deepClone(target) {
/**
* 遍歷數(shù)據(jù)處理函數(shù)
* @array 要處理的數(shù)據(jù)
* @callback 回調(diào)函數(shù),接收兩個(gè)參數(shù) value 每一項(xiàng)的值 index 每一項(xiàng)的下標(biāo)或者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]" ? [] : {};
// 解決循環(huán)引用
if (map.has(target)) {
return map.get(target);
}
map.set(target, result);
const keys = Object.prototype.toString.call(target) === "[object Array]" ? undefined : Object.keys(
target);
function callback(value, key) {
if (keys) {
// 如果keys存在則說明value是一個(gè)對(duì)象的key,不存在則說明key就是數(shù)組的下標(biāo)。
key = value;
}
result[key] = clone(target[key], map)
}
handleWhile(keys || target, callback)
return result;
} else {
return target;
}
}
let map = new WeakMap();
const result = clone(target,map);
map = null;
return result
}用 while 遍歷的深拷貝記為 deepClone,把用 for ... in 遍歷的深拷貝記為 deepClone1。利用 console.time() 和 console.timeEnd() 來計(jì)算執(zhí)行時(shí)間。
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 遍歷的深拷貝的性能遠(yuǎn)優(yōu)于用 for ... in 遍歷的深拷貝。
在這里可以向面試官展示你的五個(gè)編程能力。
- 熟悉 ES6 中 WeakMap 數(shù)據(jù)結(jié)構(gòu)的概念及應(yīng)用
- 具有優(yōu)化代碼運(yùn)行性能的能力。
- 了解遍歷的效率的能力。
- 了解
++i和i++的區(qū)別。 - 代碼抽象的能力。
星耀段位
在這個(gè)階段應(yīng)該考慮代碼邏輯的嚴(yán)謹(jǐn)性。在上面段位的代碼雖然已經(jīng)滿足平時(shí)開發(fā)的需求,但是還是有幾處邏輯不嚴(yán)謹(jǐn)?shù)牡胤健?/p>
判斷數(shù)據(jù)不是引用類型時(shí)就直接返回 target,但是原始類型中還有 Symbol 這一特殊類型的數(shù)據(jù),因?yàn)槠涿總€(gè) Symbol 都是獨(dú)一無二,需要額外拷貝處理,不能直接返回。
判斷數(shù)據(jù)是不是引用類型時(shí)不嚴(yán)謹(jǐn),漏了
typeof target === function'的判斷。只考慮了 Array、Object 兩種引用類型數(shù)據(jù)的處理,引用類型的數(shù)據(jù)還有Function 函數(shù)、Date 日期、RegExp 正則、Map 數(shù)據(jù)結(jié)構(gòu)、Set 數(shù)據(jù)機(jī)構(gòu),其中 Map 、Set 屬于 ES6 的。
廢話不多說,直接貼上全部代碼,代碼中有注釋。
function deepClone(target) {
// 獲取數(shù)據(jù)類型
function getType(target) {
return Object.prototype.toString.call(target)
}
//判斷數(shù)據(jù)是不是引用類型
function isObject(target) {
return target !== null && (typeof target === 'object' || typeof target === 'function');
}
//處理不需要遍歷的應(yīng)引用類型數(shù)據(jù)
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類型數(shù)據(jù)
function cloneSymbol(targe) {
const a = String(targe); //把Symbol字符串化
const b = a.substring(7, a.length - 1); //取出Symbol()的參數(shù)
return Symbol(b); //用原先的Symbol()的參數(shù)創(chuàng)建一個(gè)新的Symbol
}
//拷貝正則類型數(shù)據(jù)
function cloneReg(target) {
const reFlags = /\w*$/;
const result = new target.constructor(target.source, reFlags.exec(target));
result.lastIndex = target.lastIndex;
return result;
}
//拷貝函數(shù)
function cloneFunction(targe) {
//匹配函數(shù)體的正則
const bodyReg = /(?<={)(.|\n)+(?=})/m;
//匹配函數(shù)參數(shù)的正則
const paramReg = /(?<=\().+(?=\)\s+{)/;
const targeString = targe.toString();
//利用prototype來區(qū)分下箭頭函數(shù)和普通函數(shù),箭頭函數(shù)是沒有prototype的
if (targe.prototype) { //普通函數(shù)
const param = paramReg.exec(targeString);
const body = bodyReg.exec(targeString);
if (body) {
if (param) {
const paramArr = param[0].split(',');
//使用 new Function 重新構(gòu)造一個(gè)新的函數(shù)
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
} else {
return null;
}
} else { //箭頭函數(shù)
//eval和函數(shù)字符串來重新生成一個(gè)箭頭函數(shù)
return eval(targeString);
}
}
/**
* 遍歷數(shù)據(jù)處理函數(shù)
* @array 要處理的數(shù)據(jù)
* @callback 回調(diào)函數(shù),接收兩個(gè)參數(shù) value 每一項(xiàng)的值 index 每一項(xiàng)的下標(biāo)或者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();
}
// 解決循環(huán)引用
if (map.has(target)) {
return map.get(target);
}
map.set(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是一個(gè)對(duì)象的key,不存在則說明key就是數(shù)組的下標(biāo)。
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 = new WeakMap;
const result = clone(target, map);
map = null;
return result
}在這里可以向面試官展示你的六個(gè)編程能力。
- 代碼邏輯的嚴(yán)謹(jǐn)性。
- 深入了解數(shù)據(jù)類型的能力。
- JS Api 的熟練使用的能力。
- 了解箭頭函數(shù)和普通函數(shù)的區(qū)別。
- 熟練使用正則表達(dá)式的能力。
- 模塊化開發(fā)的能力
王者段位
以上代碼中還有很多數(shù)據(jù)類型的拷貝,沒有實(shí)現(xiàn),有興趣的話可以在評(píng)論中實(shí)現(xiàn)一下,王者屬于你哦!
總結(jié)
綜上所述,面試官叫你實(shí)現(xiàn)一個(gè)深拷貝,其實(shí)是要考察你各方面的能力。例如
- 白銀段位
- 對(duì)原始類型和引用類型數(shù)據(jù)的判斷能力。
- 對(duì)遞歸思維的應(yīng)用的能力。
- 黃金段位
- 正確理解引用類型概念的能力。
- 精確判斷數(shù)據(jù)類型的能力。
- 鉑金段位
- 對(duì)循環(huán)引用的理解,如何解決循環(huán)引用引起的問題的能力。
- 熟悉 ES6 中 Map 數(shù)據(jù)結(jié)構(gòu)的概念及應(yīng)用。
- 對(duì)內(nèi)存泄露的認(rèn)識(shí)和避免泄露的能力。
- 磚石段位
- 熟悉 ES6 中 WeakMap 數(shù)據(jù)結(jié)構(gòu)的概念及應(yīng)用。
- 具有優(yōu)化代碼運(yùn)行性能的能力。
- 了解遍歷的效率的能力。
- 了解
++i和i++的區(qū)別。 - 代碼抽象的能力。
- 星耀段位
- 代碼邏輯的嚴(yán)謹(jǐn)性。
- 深入了解數(shù)據(jù)類型的能力。
- JS Api 的熟練使用的能力。
- 了解箭頭函數(shù)和普通函數(shù)的區(qū)別。
- 熟練使用正則表達(dá)式的能力。
- 模塊化開發(fā)的能力
所以不要去死記硬背一些手寫代碼的面試題,最好自己動(dòng)手寫一下,看看自己達(dá)到那個(gè)段位了。
到此這篇關(guān)于前端面試的底氣之實(shí)現(xiàn)一個(gè)深拷貝的文章就介紹到這了,更多相關(guān)深拷貝實(shí)現(xiàn)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用json來定義函數(shù),在里面可以定義多個(gè)函數(shù)的實(shí)現(xiàn)方法
下面小編就為大家?guī)硪黄褂胘son來定義函數(shù),在里面可以定義多個(gè)函數(shù)的實(shí)現(xiàn)方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-10-10
JS實(shí)現(xiàn)仿京東淘寶豎排二級(jí)導(dǎo)航
本文給大家分享一段使用原生Javascript實(shí)現(xiàn)的仿京東淘寶豎排二級(jí)導(dǎo)航的代碼,非常的實(shí)用,有需要的小伙伴參考下2014-12-12
判斷字符串的長(zhǎng)度(優(yōu)化版)中文占兩個(gè)字符
判斷字符串的長(zhǎng)度的方法有很多,本例介紹的是優(yōu)化之前的方法,記住中文占兩個(gè)字符,需要的朋友不要錯(cuò)過2014-10-10
微信小程序?qū)崿F(xiàn)加入購物車滑動(dòng)軌跡
這篇文章主要為大家詳細(xì)介紹了微信小程序?qū)崿F(xiàn)加入購物車滑動(dòng)軌跡,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-11-11
微信小程序 導(dǎo)入圖標(biāo)實(shí)現(xiàn)過程詳解
這篇文章主要介紹了微信小程序 導(dǎo)入圖標(biāo)實(shí)現(xiàn)過程詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-10-10

