如何實現深拷貝


005:如何寫一個完整的深拷貝?

上一篇已經解釋了什么是深拷貝,現在我們來一起實現一個完整且專業的深拷貝。

#1. 簡易版及問題

JSON.parse(JSON.stringify()); 

估計這個api能覆蓋大多數的應用場景,沒錯,談到深拷貝,我第一個想到的也是它。但是實際上,對於某些嚴格的場景來說,這個方法是有巨大的坑的。問題如下:

WARNING

  1. 無法解決循環引用的問題。舉個例子:
const a = {val:2}; a.target = a; 

拷貝a會出現系統棧溢出,因為出現了無限遞歸的情況。

  1. 無法拷貝一寫特殊的對象,諸如 RegExp, Date, Set, Map等。

  2. 無法拷貝函數(划重點)。

因此這個api先pass掉,我們重新寫一個深拷貝,簡易版如下:

const deepClone = (target) => { if (typeof target === 'object' && target !== null) { const cloneTarget = Array.isArray(target) ? []: {}; for (let prop in target) { if (target.hasOwnProperty(prop)) { cloneTarget[prop] = deepClone(target[prop]); } } return cloneTarget; } else { return target; } } 

現在,我們以剛剛發現的三個問題為導向,一步步來完善、優化我們的深拷貝代碼。

#2. 解決循環引用

現在問題如下:

let obj = {val : 100}; obj.target = obj; deepClone(obj);//報錯: RangeError: Maximum call stack size exceeded 

這就是循環引用。我們怎么來解決這個問題呢?

創建一個Map。記錄下已經拷貝過的對象,如果說已經拷貝過,那直接返回它行了。

const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null; const deepClone = (target, map = new Map()) => { if(map.get(target)) return target; if (isObject(target)) { map.put(target, true); const cloneTarget = Array.isArray(target) ? []: {}; for (let prop in target) { if (target.hasOwnProperty(prop)) { cloneTarget[prop] = deepClone(target[prop]); } } return cloneTarget; } else { return target; } } 

現在來試一試:

const a = {val:2}; a.target = a; let newA = deepClone(a); console.log(newA)//{ val: 2, target: { val: 2, target: [Circular] } } 

好像是沒有問題了, 拷貝也完成了。但還是有一個潛在的坑, 就是map 上的 key 和 map 構成了強引用關系,這是相當危險的。我給你解釋一下與之相對的弱引用的概念你就明白了:

在計算機程序設計中,弱引用與強引用相對, 是指不能確保其引用的對象不會被垃圾回收器回收的引用。 一個對象若只被弱引用所引用,則被認為是不可訪問(或弱可訪問)的,並因此可能在任何時刻被回收。 --百度百科

說的有一點繞,我用大白話解釋一下,被弱引用的對象可以在任何時候被回收,而對於強引用來說,只要這個強引用還在,那么對象無法被回收。拿上面的例子說,map 和 a一直是強引用的關系, 在程序結束之前,a 所占的內存空間一直不會被釋放

怎么解決這個問題?

很簡單,讓 map 的 key 和 map 構成弱引用即可。ES6給我們提供了這樣的數據結構,它的名字叫WeakMap,它是一種特殊的Map, 其中的鍵是弱引用的。其鍵必須是對象,而值可以是任意的。

稍微改造一下即可:

const deepClone = (target, map = new Map()) => { //... } 

#3. 拷貝特殊對象

#可繼續遍歷

對於特殊的對象,我們使用以下方式來鑒別:

Object.prototype.toString.call(obj); 

梳理一下對於可遍歷對象會有什么結果:

["object Map"] ["object Set"] ["object Array"] ["object Object"] ["object Arguments"] 

好,以這些不同的字符串為依據,我們就可以成功地鑒別這些對象。

const getType = Object.prototype.toString.call(obj); const canTraverse = { '[object Map]': true, '[object Set]': true, '[object Array]': true, '[object Object]': true, '[object Arguments]': true, }; const deepClone = (target, map = new Map()) => { if(!isObject(target)) return target; let type = getType(target); let cloneTarget; if(!canTraverse[type]) { // 處理不能遍歷的對象 return; }else { // 這波操作相當關鍵,可以保證對象的原型不丟失! let ctor = target.prototype; cloneTarget = new ctor(); } if(map.get(target)) return target; map.put(target, true); if(type === mapTag) { //處理Map target.forEach((item, key) => { cloneTarget.set(deepClone(key), deepClone(item)); }) } if(type === setTag) { //處理Set target.forEach(item => { target.add(deepClone(item)); }) } // 處理數組和對象 for (let prop in target) { if (target.hasOwnProperty(prop)) { cloneTarget[prop] = deepClone(target[prop]); } } return cloneTarget; } 

#不可遍歷的對象

const boolTag = '[object Boolean]'; const numberTag = '[object Number]'; const stringTag = '[object String]'; const dateTag = '[object Date]'; const errorTag = '[object Error]'; const regexpTag = '[object RegExp]'; const funcTag = '[object Function]'; 

對於不可遍歷的對象,不同的對象有不同的處理。

const handleRegExp = (target) => { const { source, flags } = target; return new target.constructor(source, flags); } const handleFunc = (target) => { // 待會的重點部分 } const handleNotTraverse = (target, tag) => { const Ctor = targe.constructor; switch(tag) { case boolTag: case numberTag: case stringTag: case errorTag: case dateTag: return new Ctor(target); case regexpTag: return handleRegExp(target); case funcTag: return handleFunc(target); default: return new Ctor(target); } } 

#4. 拷貝函數

雖然函數也是對象,但是它過於特殊,我們單獨把它拿出來拆解。

提到函數,在JS種有兩種函數,一種是普通函數,另一種是箭頭函數。每個普通函數都是 Function的實例,而箭頭函數不是任何類的實例,每次調用都是不一樣的引用。那我們只需要 處理普通函數的情況,箭頭函數直接返回它本身就好了。

那么如何來區分兩者呢?

答案是: 利用原型。箭頭函數是不存在原型的。

代碼如下:

const handleFunc = (func) => { // 箭頭函數直接返回自身 if(!func.prototype) return func; const bodyReg = /(?<={)(.|\n)+(?=})/m; const paramReg = /(?<=\().+(?=\)\s+{)/; const funcString = func.toString(); // 分別匹配 函數參數 和 函數體 const param = paramReg.exec(funcString); const body = bodyReg.exec(funcString); if(!body) return null; if (param) { const paramArr = param[0].split(','); return new Function(...paramArr, body[0]); } else { return new Function(body[0]); } } 

到現在,我們的深拷貝就實現地比較完善了。不過在測試的過程中,我也發現了一個小小的bug。

#5. 小小的bug

如下所示:

const target = new Boolean(false); const Ctor = target.constructor; new Ctor(target); // 結果為 Boolean {true} 而不是 false。 

對於這樣一個bug,我們可以對 Boolean 拷貝做最簡單的修改, 調用valueOf: new target.constructor(target.valueOf())。

但實際上,這種寫法是不推薦的。因為在ES6后不推薦使用【new 基本類型()】這 樣的語法,所以es6中的新類型 Symbol 是不能直接 new 的,只能通過 new Object(SymbelType)。

因此我們接下來統一一下:

const handleNotTraverse = (target, tag) => { const Ctor = targe.constructor; switch(tag) { case boolTag: return new Object(Boolean.prototype.valueOf.call(target)); case numberTag: return new Object(Number.prototype.valueOf.call(target)); case stringTag: return new Object(String.prototype.valueOf.call(target)); case errorTag: case dateTag: return new Ctor(target); case regexpTag: return handleRegExp(target); case funcTag: return handleFunc(target); default: return new Ctor(target); } } 

OK!是時候給大家放出完整版的深拷貝啦:

const getType = obj => Object.prototype.toString.call(obj); const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null; const canTraverse = { '[object Map]': true, '[object Set]': true, '[object Array]': true, '[object Object]': true, '[object Arguments]': true, }; const mapTag = '[object Map]'; const setTag = '[object Set]'; const boolTag = '[object Boolean]'; const numberTag = '[object Number]'; const stringTag = '[object String]'; const symbolTag = '[object Symbol]'; const dateTag = '[object Date]'; const errorTag = '[object Error]'; const regexpTag = '[object RegExp]'; const funcTag = '[object Function]'; const handleRegExp = (target) => { const { source, flags } = target; return new target.constructor(source, flags); } const handleFunc = (func) => { // 箭頭函數直接返回自身 if(!func.prototype) return func; const bodyReg = /(?<={)(.|\n)+(?=})/m; const paramReg = /(?<=\().+(?=\)\s+{)/; const funcString = func.toString(); // 分別匹配 函數參數 和 函數體 const param = paramReg.exec(funcString); const body = bodyReg.exec(funcString); if(!body) return null; if (param) { const paramArr = param[0].split(','); return new Function(...paramArr, body[0]); } else { return new Function(body[0]); } } const handleNotTraverse = (target, tag) => { const Ctor = target.constructor; switch(tag) { case boolTag: return new Object(Boolean.prototype.valueOf.call(target)); case numberTag: return new Object(Number.prototype.valueOf.call(target)); case stringTag: return new Object(String.prototype.valueOf.call(target)); case symbolTag: return new Object(Symbol.prototype.valueOf.call(target)); case errorTag: case dateTag: return new Ctor(target); case regexpTag: return handleRegExp(target); case funcTag: return handleFunc(target); default: return new Ctor(target); } } const deepClone = (target, map = new Map()) => { if(!isObject(target)) return target; let type = getType(target); let cloneTarget; if(!canTraverse[type]) { // 處理不能遍歷的對象 return handleNotTraverse(target, type); }else { // 這波操作相當關鍵,可以保證對象的原型不丟失! let ctor = target.constructor; cloneTarget = new ctor(); } if(map.get(target)) return target; map.set(target, true); if(type === mapTag) { //處理Map target.forEach((item, key) => { cloneTarget.set(deepClone(key, map), deepClone(item, map)); }) } if(type === setTag) { //處理Set target.forEach(item => { cloneTarget.add(deepClone(item, map)); }) } // 處理數組和對象 for (let prop in target) { if (target.hasOwnProperty(prop)) { cloneTarget[prop] = deepClone(target[prop], map); } } return cloneTarget; }


免責聲明!

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



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