基礎鞏固-你最少用幾行代碼實現(xiàn)深拷貝?
點擊上方?前端Q,關注公眾號
回復加群,加入前端Q技術交流群
前言
深度克?。ㄉ羁截悾┮恢倍际浅?、中級前端面試中經(jīng)常被問到的題目,網(wǎng)上介紹的實現(xiàn)方式也都各有千秋,大體可以概括為三種方式:
JSON.stringify+JSON.parse, 這個很好理解;全量判斷類型,根據(jù)類型做不同的處理 2的變型,簡化類型判斷過程
前兩種比較常見也比較基礎,所以我們今天主要討論的是第三種。
閱讀全文你將學習到:
更簡潔的深度克隆方式 Object.getOwnPropertyDescriptors()api類型判斷的通用方法
問題分析
深拷貝 自然是 相對 淺拷貝 而言的。我們都知道 引用數(shù)據(jù)類型 變量存儲的是數(shù)據(jù)的引用,就是一個指向內存空間的指針, 所以如果我們像賦值簡單數(shù)據(jù)類型那樣的方式賦值的話,其實只能復制一個指針引用,并沒有實現(xiàn)真正的數(shù)據(jù)克隆。
通過這個例子很容易就能理解:
const?obj1?=?{
????name:?'superman'
}
const?obj2?=?obj1;
obj1.name?=?'前端切圖仔';
console.log(obj2.name);?//?前端切圖仔
復制代碼
所以深度克隆就是為了解決引用數(shù)據(jù)類型不能被通過賦值的方式 復制 的問題。
引用數(shù)據(jù)類型
我們不妨來羅列一下引用數(shù)據(jù)類型都有哪些:
ES6之前:Object, Array, Date, RegExp, Error, ES6之后:Map, Set, WeakMap, WeakSet,
所以,我們要深度克隆,就需要對數(shù)據(jù)進行遍歷并根據(jù)類型采取相應的克隆方式。當然因為數(shù)據(jù)會存在多層嵌套的情況,采用遞歸是不錯的選擇。
簡單粗暴版本
function?deepClone(obj)?{
????let?res?=?{};
????//?類型判斷的通用方法
????function?getType(obj)?{
????????return?Object.prototype.toString.call(obj).replaceAll(new?RegExp(/\[|\]|object?/g),?"");
????}
????const?type?=?getType(obj);
????const?reference?=?["Set",?"WeakSet",?"Map",?"WeakMap",?"RegExp",?"Date",?"Error"];
????if?(type?===?"Object")?{
????????for?(const?key?in?obj)?{
????????????if?(Object.hasOwnProperty.call(obj,?key))?{
????????????????res[key]?=?deepClone(obj[key]);
????????????}
????????}
????}?else?if?(type?===?"Array")?{
????????console.log('array?obj',?obj);
????????obj.forEach((e,?i)?=>?{
????????????res[i]?=?deepClone(e);
????????});
????}
????else?if?(type?===?"Date")?{
????????res?=?new?Date(obj);
????}?else?if?(type?===?"RegExp")?{
????????res?=?new?RegExp(obj);
????}?else?if?(type?===?"Map")?{
????????res?=?new?Map(obj);
????}?else?if?(type?===?"Set")?{
????????res?=?new?Set(obj);
????}?else?if?(type?===?"WeakMap")?{
????????res?=?new?WeakMap(obj);
????}?else?if?(type?===?"WeakSet")?{
????????res?=?new?WeakSet(obj);
????}else?if?(type?===?"Error")?{
????????res?=?new?Error(obj);
????}
?????else?{
????????res?=?obj;
????}
????return?res;
}
復制代碼
其實這就是我們最前面提到的第二種方式,很傻對不對,明眼人一眼就能看出來有很多冗余代碼可以合并。
我們先進行最基本的優(yōu)化:
合并冗余代碼
將一眼就能看出來冗余的代碼合并下。
function?deepClone(obj)?{
????let?res?=?null;
????//?類型判斷的通用方法
????function?getType(obj)?{
????????return?Object.prototype.toString.call(obj).replaceAll(new?RegExp(/\[|\]|object?/g),?"");
????}
????const?type?=?getType(obj);
????const?reference?=?["Set",?"WeakSet",?"Map",?"WeakMap",?"RegExp",?"Date",?"Error"];
????if?(type?===?"Object")?{
????????res?=?{};
????????for?(const?key?in?obj)?{
????????????if?(Object.hasOwnProperty.call(obj,?key))?{
????????????????res[key]?=?deepClone(obj[key]);
????????????}
????????}
????}?else?if?(type?===?"Array")?{
????????console.log('array?obj',?obj);
????????res?=?[];
????????obj.forEach((e,?i)?=>?{
????????????res[i]?=?deepClone(e);
????????});
????}
????//?優(yōu)化此部分冗余判斷
????//?else?if?(type?===?"Date")?{
????//?????res?=?new?Date(obj);
????//?}?else?if?(type?===?"RegExp")?{
????//?????res?=?new?RegExp(obj);
????//?}?else?if?(type?===?"Map")?{
????//?????res?=?new?Map(obj);
????//?}?else?if?(type?===?"Set")?{
????//?????res?=?new?Set(obj);
????//?}?else?if?(type?===?"WeakMap")?{
????//?????res?=?new?WeakMap(obj);
????//?}?else?if?(type?===?"WeakSet")?{
????//?????res?=?new?WeakSet(obj);
????//?}else?if?(type?===?"Error")?{
????//???res?=?new?Error(obj);
????//}
????else?if?(reference.includes(type))?{
????????res?=?new?obj.constructor(obj);
????}?else?{
????????res?=?obj;
????}
????return?res;
}
復制代碼
為了驗證代碼的正確性,我們用下面這個數(shù)據(jù)驗證下:
const?map?=?new?Map();
map.set("key",?"value");
map.set("ConardLi",?"coder");
const?set?=?new?Set();
set.add("ConardLi");
set.add("coder");
const?target?=?{
????field1:?1,
????field2:?undefined,
????field3:?{
????????child:?"child",
????},
????field4:?[2,?4,?8],
????empty:?null,
????map,
????set,
????bool:?new?Boolean(true),
????num:?new?Number(2),
????str:?new?String(2),
????symbol:?Object(Symbol(1)),
????date:?new?Date(),
????reg:?/\d+/,
????error:?new?Error(),
????func1:?()?=>?{
????????let?t?=?0;
????????console.log("coder",?t++);
????},
????func2:?function?(a,?b)?{
????????return?a?+?b;
????},
};
//測試代碼
const?test1?=?deepClone(target);
target.field4.push(9);
console.log('test1:?',?test1);
復制代碼
執(zhí)行結果:

還有進一步優(yōu)化的空間嗎?
答案當然是肯定的。
//?判斷類型的方法移到外部,避免遞歸過程中多次執(zhí)行
const?judgeType?=?origin?=>?{
????return?Object.prototype.toString.call(origin).replaceAll(new?RegExp(/\[|\]|object?/g),?"");
};
const?reference?=?["Set",?"WeakSet",?"Map",?"WeakMap",?"RegExp",?"Date",?"Error"];
function?deepClone(obj)?{
????//?定義新的對象,最后返回
?????//通過?obj?的原型創(chuàng)建對象
????const?cloneObj?=?Object.create(Object.getPrototypeOf(obj),?Object.getOwnPropertyDescriptors(obj));
????//?遍歷對象,克隆屬性
????for?(let?key?of?Reflect.ownKeys(obj))?{
????????const?val?=?obj[key];
????????const?type?=?judgeType(val);
????????if?(reference.includes(type))?{
????????????newObj[key]?=?new?val.constructor(val);
????????}?else?if?(typeof?val?===?"object"?&&?val?!==?null)?{
????????????//?遞歸克隆
????????????newObj[key]?=?deepClone(val);
????????}?else?{
????????????//?基本數(shù)據(jù)類型和function
????????????newObj[key]?=?val;
????????}
????}
????return?newObj;
}
復制代碼
執(zhí)行結果如下:

Object.getOwnPropertyDescriptors()?方法用來獲取一個對象的所有自身屬性的描述符。返回所指定對象的所有自身屬性的描述符,如果沒有任何自身屬性,則返回空對象。
具體解釋和內容見MDN[1]
這樣做的好處就是能夠提前定義好最后返回的數(shù)據(jù)類型。
這個實現(xiàn)參考了網(wǎng)上一位大佬的實現(xiàn)方式,個人覺得理解成本有點高,而且對數(shù)組類型的處理也不是特別優(yōu)雅, 返回類數(shù)組。
我在我上面代碼的基礎上進行了改造,改造后的代碼如下:
function?deepClone(obj)?{
????let?res?=?null;
????const?reference?=?[Date,?RegExp,?Set,?WeakSet,?Map,?WeakMap,?Error];
????if?(reference.includes(obj?.constructor))?{
????????res?=?new?obj.constructor(obj);
????}?else?if?(Array.isArray(obj))?{
????????res?=?[];
????????obj.forEach((e,?i)?=>?{
????????????res[i]?=?deepClone(e);
????????});
????}?else?if?(typeof?obj?===?"object"?&&?obj?!==?null)?{
????????res?=?{};
????????for?(const?key?in?obj)?{
????????????if?(Object.hasOwnProperty.call(obj,?key))?{
????????????????res[key]?=?deepClone(obj[key]);
????????????}
????????}
????}?else?{
????????res?=?obj;
????}
????return?res;
}
復制代碼
雖然代碼量上沒有什么優(yōu)勢,但是整體的理解成本和你清晰度上我覺得會更好一點。那么你覺得呢?
最后,還有循環(huán)引用問題,避免出現(xiàn)無線循環(huán)的問題。
我們用hash來存儲已經(jīng)加載過的對象,如果已經(jīng)存在的對象,就直接返回。
function?deepClone(obj,?hash?=?new?WeakMap())?{
????if?(hash.has(obj))?{
????????return?obj;
????}
????let?res?=?null;
????const?reference?=?[Date,?RegExp,?Set,?WeakSet,?Map,?WeakMap,?Error];
????if?(reference.includes(obj?.constructor))?{
????????res?=?new?obj.constructor(obj);
????}?else?if?(Array.isArray(obj))?{
????????res?=?[];
????????obj.forEach((e,?i)?=>?{
????????????res[i]?=?deepClone(e);
????????});
????}?else?if?(typeof?obj?===?"object"?&&?obj?!==?null)?{
????????res?=?{};
????????for?(const?key?in?obj)?{
????????????if?(Object.hasOwnProperty.call(obj,?key))?{
????????????????res[key]?=?deepClone(obj[key]);
????????????}
????????}
????????hash.set(obj,?res);
????}?else?{
????????res?=?obj;
????}
????return?res;
}
復制代碼
總結
對于深拷貝的實現(xiàn),可能存在很多不同的實現(xiàn)方式,關鍵在于理解其原理,并能夠記住一種最容易理解和實現(xiàn)的方式,面對類似的問題才能做到 臨危不亂,泰然自若。上面的實現(xiàn)你覺得哪個更好呢?歡迎大佬們在評論區(qū)交流~
更文不易, 看完記得點個贊支持一下哦~ 這將是我寫作的動力源泉~
關于本文
作者:前端superman
https://juejin.cn/post/7075351322014253064

往期推薦



最后
歡迎加我微信,拉你進技術群,長期交流學習...
歡迎關注「前端Q」,認真學前端,做個專業(yè)的技術人...


