面试题:如何实现一个深拷贝
轉載于:元光木易楊?前端大全?20190228
如何實現一個深拷貝
第一步:簡單實現
其實深拷貝可以拆分成 2 步,淺拷貝 + 遞歸,淺拷貝時判斷屬性值是否是對象,如果是對象就進行遞歸操作,兩個一結合就實現了深拷貝。
根據上篇文章內容,我們可以寫出簡單淺拷貝代碼如下。
//?木易楊 function?cloneShallow(source)?{var?target?=?{};for?(var?key?in?source)?{if?(Object.prototype.hasOwnProperty.call(source,?key))?{target[key]?=?source[key];}}return?target; }//?測試用例 var?a?=?{name:?"muyiy",book:?{title:?"You?Don't?Know?JS",price:?"45"},a1:?undefined,a2:?null,a3:?123 } var?b?=?cloneShallow(a);a.name?=?"高級前端進階"; a.book.price?=?"55";console.log(b); //?{? //???name:?'muyiy',? //???book:?{?title:?'You?Don\'t?Know?JS',?price:?'55'?}, //???a1:?undefined, //???a2:?null, //???a3:?123 //?}上面代碼是淺拷貝實現,只要稍微改動下,加上是否是對象的判斷并在相應的位置使用遞歸就可以實現簡單深拷貝。
//?木易楊 function?cloneDeep1(source)?{var?target?=?{};for(var?key?in?source)?{if?(Object.prototype.hasOwnProperty.call(source,?key))?{if?(typeof?source[key]?===?'object')?{target[key]?=?cloneDeep1(source[key]);?//?注意這里}?else?{target[key]?=?source[key];}}}return?target; }//?使用上面測試用例測試一下 var?b?=?cloneDeep1(a); console.log(b); //?{? //???name:?'muyiy',? //???book:?{?title:?'You?Don\'t?Know?JS',?price:?'45'?},? //???a1:?undefined, //???a2:?{}, //???a3:?123 //?}一個簡單的深拷貝就完成了,但是這個實現還存在很多問題。
-
1、沒有對傳入參數進行校驗,傳入?null?時應該返回?null?而不是?{}
-
2、對于對象的判斷邏輯不嚴謹,因為?typeof null === 'object'
-
3、沒有考慮數組的兼容
第二步:拷貝數組
我們來看下對于對象的判斷,之前在【進階3-3期】有過介紹,判斷方案如下。
//?木易楊 function?isObject(obj)?{return?Object.prototype.toString.call(obj)?===?'[object?Object]'; }但是用在這里并不合適,因為我們要保留數組這種情況,所以這里使用?typeof?來處理。
//?木易楊 typeof?null?//"object" typeof?{}?//"object" typeof?[]?//"object" typeof?function?foo(){}?//"function"?(特殊情況)改動過后的 isObject 判斷邏輯如下。
//?木易楊 function?isObject(obj)?{return?typeof?obj?===?'object'?&&?obj?!=?null; }所以兼容數組的寫法如下。
//?木易楊 function?cloneDeep2(source)?{if?(!isObject(source))?return?source;?//?非對象返回自身var?target?=?Array.isArray(source)???[]?:?{};for(var?key?in?source)?{if?(Object.prototype.hasOwnProperty.call(source,?key))?{if?(isObject(source[key]))?{target[key]?=?cloneDeep2(source[key]);?//?注意這里}?else?{target[key]?=?source[key];}}}return?target; }//?使用上面測試用例測試一下 var?b?=?cloneDeep2(a); console.log(b); //?{? //???name:?'muyiy',? //???book:?{?title:?'You?Don\'t?Know?JS',?price:?'45'?}, //???a1:?undefined, //???a2:?null, //???a3:?123 //?}第三步:循環引用
我們知道?JSON?無法深拷貝循環引用,遇到這種情況會拋出異常。
//?木易楊 //?此處?a?是文章開始的測試用例 a.circleRef?=?a;JSON.parse(JSON.stringify(a)); //?TypeError:?Converting?circular?structure?to?JSON1、使用哈希表
解決方案很簡單,其實就是循環檢測,我們設置一個數組或者哈希表存儲已拷貝過的對象,當檢測到當前對象已存在于哈希表中時,取出該值并返回即可。
//?木易楊 function?cloneDeep3(source,?hash?=?new?WeakMap())?{if?(!isObject(source))?return?source;?if?(hash.has(source))?return?hash.get(source);?//?新增代碼,查哈希表var?target?=?Array.isArray(source)???[]?:?{};hash.set(source,?target);?//?新增代碼,哈希表設值for(var?key?in?source)?{if?(Object.prototype.hasOwnProperty.call(source,?key))?{if?(isObject(source[key]))?{target[key]?=?cloneDeep3(source[key],?hash);?//?新增代碼,傳入哈希表}?else?{target[key]?=?source[key];}}}return?target; }測試一下,看看效果如何。
//?木易楊 //?此處?a?是文章開始的測試用例 a.circleRef?=?a;var?b?=?cloneDeep3(a); console.log(b); //?{ //?????name:?"muyiy", //?????a1:?undefined, //????a2:?null, //?????a3:?123, //?????book:?{title:?"You?Don't?Know?JS",?price:?"45"}, //?????circleRef:?{name:?"muyiy",?book:?{…},?a1:?undefined,?a2:?null,?a3:?123,?…} //?}完美!
2、使用數組
這里使用了ES6?中的?WeakMap?來處理,那在?ES5?下應該如何處理呢?
也很簡單,使用數組來處理就好啦,代碼如下。
//?木易楊 function?cloneDeep3(source,?uniqueList)?{if?(!isObject(source))?return?source;?if?(!uniqueList)?uniqueList?=?[];?//?新增代碼,初始化數組var?target?=?Array.isArray(source)???[]?:?{};//?=============?新增代碼//?數據已經存在,返回保存的數據var?uniqueData?=?find(uniqueList,?source);if?(uniqueData)?{return?uniqueData.target;};//?數據不存在,保存源數據,以及對應的引用uniqueList.push({source:?source,target:?target});//?=============for(var?key?in?source)?{if?(Object.prototype.hasOwnProperty.call(source,?key))?{if?(isObject(source[key]))?{target[key]?=?cloneDeep3(source[key],?uniqueList);?//?新增代碼,傳入數組}?else?{target[key]?=?source[key];}}}return?target; }//?新增方法,用于查找 function?find(arr,?item)?{for(var?i?=?0;?i?<?arr.length;?i++)?{if?(arr[i].source?===?item)?{return?arr[i];}}return?null; }//?用上面測試用例已測試通過現在已經很完美的解決了循環引用這種情況,那其實還是一種情況是引用丟失,我們看下面的例子。
//?木易楊 var?obj1?=?{}; var?obj2?=?{a:?obj1,?b:?obj1};obj2.a?===?obj2.b;? //?truevar?obj3?=?cloneDeep2(obj2); obj3.a?===?obj3.b;? //?false引用丟失在某些情況下是有問題的,比如上面的對象 obj2,obj2 的鍵值 a 和 b 同時引用了同一個對象 obj1,使用 cloneDeep2 進行深拷貝后就丟失了引用關系變成了兩個不同的對象,那如何處理呢。
其實你有沒有發現,我們的 cloneDeep3 已經解決了這個問題,因為只要存儲已拷貝過的對象就可以了。
//?木易楊 var?obj3?=?cloneDeep3(obj2); obj3.a?===?obj3.b;? //?true完美!
第四步:拷貝 Symbol
這個時候可能要搞事情了,那我們能不能拷貝 Symol 類型呢?
當然可以,不過?Symbol?在?ES6?下才有,我們需要一些方法來檢測出?Symble?類型。
方法一:Object.getOwnPropertySymbols(...)
方法二:Reflect.ownKeys(...)
對于方法一可以查找一個給定對象的符號屬性時返回一個??symbol?類型的數組。注意,每個初始化的對象都是沒有自己的?symbol?屬性的,因此這個數組可能為空,除非你已經在對象上設置了?symbol?屬性。(來自MDN)
var?obj?=?{}; var?a?=?Symbol("a");?//?創建新的symbol類型 var?b?=?Symbol.for("b");?//?從全局的symbol注冊?表設置和取得symbolobj[a]?=?"localSymbol"; obj[b]?=?"globalSymbol";var?objectSymbols?=?Object.getOwnPropertySymbols(obj);console.log(objectSymbols.length);?//?2 console.log(objectSymbols)?????????//?[Symbol(a),?Symbol(b)] console.log(objectSymbols[0])??????//?Symbol(a)對于方法二返回一個由目標對象自身的屬性鍵組成的數組。它的返回值等同于Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))。(來自MDN)
Reflect.ownKeys({z:?3,?y:?2,?x:?1});?//?[?"z",?"y",?"x"?] Reflect.ownKeys([]);?//?["length"]var?sym?=?Symbol.for("comet"); var?sym2?=?Symbol.for("meteor"); var?obj?=?{[sym]:?0,?"str":?0,?"773":?0,?"0":?0,[sym2]:?0,?"-1":?0,?"8":?0,?"second?str":?0}; Reflect.ownKeys(obj); //?[?"0",?"8",?"773",?"str",?"-1",?"second?str",?Symbol(comet),?Symbol(meteor)?] //?注意順序 //?Indexes?in?numeric?order,? //?strings?in?insertion?order,? //?symbols?in?insertion?order方法一
思路就是先查找有沒有?Symbol?屬性,如果查找到則先遍歷處理?Symbol?情況,然后再處理正常情況,多出來的邏輯就是下面的新增代碼。
//?木易楊 function?cloneDeep4(source,?hash?=?new?WeakMap())?{if?(!isObject(source))?return?source;?if?(hash.has(source))?return?hash.get(source);?let?target?=?Array.isArray(source)???[]?:?{};hash.set(source,?target);//?=============?新增代碼let?symKeys?=?Object.getOwnPropertySymbols(source);?//?查找if?(symKeys.length)?{?//?查找成功????symKeys.forEach(symKey?=>?{if?(isObject(source[symKey]))?{target[symKey]?=?cloneDeep4(source[symKey],?hash);?}?else?{target[symKey]?=?source[symKey];}????});}//?=============for(let?key?in?source)?{if?(Object.prototype.hasOwnProperty.call(source,?key))?{if?(isObject(source[key]))?{target[key]?=?cloneDeep4(source[key],?hash);?}?else?{target[key]?=?source[key];}}}return?target; }測試下效果
//?木易楊 //?此處?a?是文章開始的測試用例 var?sym1?=?Symbol("a");?//?創建新的symbol類型 var?sym2?=?Symbol.for("b");?//?從全局的symbol注冊?表設置和取得symbola[sym1]?=?"localSymbol"; a[sym2]?=?"globalSymbol";var?b?=?cloneDeep4(a); console.log(b); //?{ //?????name:?"muyiy", //?????a1:?undefined, //????a2:?null, //?????a3:?123, //?????book:?{title:?"You?Don't?Know?JS",?price:?"45"}, //?????circleRef:?{name:?"muyiy",?book:?{…},?a1:?undefined,?a2:?null,?a3:?123,?…}, //??[Symbol(a)]:?'localSymbol', //??[Symbol(b)]:?'globalSymbol' //?}完美!
方法二
//?木易楊 function?cloneDeep4(source,?hash?=?new?WeakMap())?{if?(!isObject(source))?return?source;?if?(hash.has(source))?return?hash.get(source);?let?target?=?Array.isArray(source)???[...source]?:?{?...source?};?//?改動?1hash.set(source,?target);Reflect.ownKeys(target).forEach(key?=>?{?//?改動?2if?(isObject(source[key]))?{target[key]?=?cloneDeep4(source[key],?hash);?}?else?{target[key]?=?source[key];}??});return?target; }//?測試已通過這里說一下改動 1,因為使用?Reflect.ownKeys()?獲取所有的鍵值,所以 target 需要改成如上所示,返回一個新數組或者新對象,獲取到源對象之后就可以如改動 2 所示遍歷賦值即可。
這種方式的問題在于不能深拷貝原型鏈上的數據,因為?Reflect.ownKeys()?返回的是目標對象自身的屬性鍵組成的數組。如果想深拷貝原型鏈上的數據怎么辦,那用?for..in?就可以了。
完美!
我們延伸下介紹以下兩個知識點,分別是構造字面量數組時使用展開語法和構造字面量對象時使用展開語法。(以下代碼示例來源于 MDN)
1、展開語法之字面量數組
這是?ES2015 (ES6)?才有的語法,可以通過字面量方式, 構造新數組,而不再需要組合使用 ?push,?splice,?concat?等方法。
var?parts?=?['shoulders',?'knees'];? var?lyrics?=?['head',?...parts,?'and',?'toes'];? //?["head",?"shoulders",?"knees",?"and",?"toes"]這里的使用方法和參數列表的展開有點類似。
function?myFunction(v,?w,?x,?y,?z)?{?} var?args?=?[0,?1]; myFunction(-1,?...args,?2,?...[3]);返回的是新數組,對新數組修改之后不會影響到舊數組,類似于?arr.slice()。
var?arr?=?[1,?2,?3]; var?arr2?=?[...arr];?//?like?arr.slice() arr2.push(4);?//?arr2?此時變成?[1,?2,?3,?4] //?arr?不受影響展開語法和?Object.assign()?行為一致, 執行的都是淺拷貝(即只遍歷一層)。
var?a?=?[[1],?[2],?[3]]; var?b?=?[...a]; b.shift().shift();?//?1 //?[[],?[2],?[3]]這里 a 是多層數組,b 只拷貝了第一層,對于第二層依舊和 a 持有同一個地址,所以對 b 的修改會影響到 a。
2、展開語法之字面量對象
這是?ES2018?才有的語法,將已有對象的所有可枚舉屬性拷貝到新構造的對象中,類似于 ?Object.assign()?方法。
var?obj1?=?{?foo:?'bar',?x:?42?}; var?obj2?=?{?foo:?'baz',?y:?13?};var?clonedObj?=?{?...obj1?}; //?{?foo:?"bar",?x:?42?}var?mergedObj?=?{?...obj1,?...obj2?}; //?{?foo:?"baz",?x:?42,?y:?13?}Object.assign()?函數會觸發 setters,而展開語法不會。有時候不能替換或者模擬?Object.assign()?函數,因為會得到意想不到的結果,如下所示。
var?obj1?=?{?foo:?'bar',?x:?42?}; var?obj2?=?{?foo:?'baz',?y:?13?}; const?merge?=?(?...objects?)?=>?(?{?...objects?}?);var?mergedObj?=?merge?(?obj1,?obj2); //?{?0:?{?foo:?'bar',?x:?42?},?1:?{?foo:?'baz',?y:?13?}?}var?mergedObj?=?merge?(?{},?obj1,?obj2); //?{?0:?{},?1:?{?foo:?'bar',?x:?42?},?2:?{?foo:?'baz',?y:?13?}?}這里實際上是將多個解構變為剩余參數(?rest?),然后再將剩余參數展開為字面量對象.
第五步:破解遞歸爆棧
上面四步使用的都是遞歸方法,但是有一個問題在于會爆棧,錯誤提示如下。
//?RangeError:?Maximum?call?stack?size?exceeded那應該如何解決呢?其實我們使用循環就可以了,代碼如下。
function?cloneDeep5(x)?{const?root?=?{};//?棧const?loopList?=?[{parent:?root,key:?undefined,data:?x,}];while(loopList.length)?{//?深度優先const?node?=?loopList.pop();const?parent?=?node.parent;const?key?=?node.key;const?data?=?node.data;//?初始化賦值目標,key為undefined則拷貝到父元素,否則拷貝到子元素let?res?=?parent;if?(typeof?key?!==?'undefined')?{res?=?parent[key]?=?{};}for(let?k?in?data)?{if?(data.hasOwnProperty(k))?{if?(typeof?data[k]?===?'object')?{//?下一次循環loopList.push({parent:?res,key:?k,data:?data[k],});}?else?{res[k]?=?data[k];}}}}return?root; }總結
以上是生活随笔為你收集整理的面试题:如何实现一个深拷贝的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 公关是干嘛的
- 下一篇: 2021年机关作风效能建设工作意见3篇