你最少用幾行代碼實現深拷貝?
前言
深度克隆(深拷貝)一直都是初、中級前端面試中經常被問到的題目,網上介紹的實現方式也都各有千秋,大體可以概括爲三種方式:
-
JSON.stringify+JSON.parse
, 這個很好理解; -
全量判斷類型,根據類型做不同的處理
-
2 的變型,簡化類型判斷過程
前兩種比較常見也比較基礎,所以我們今天主要討論的是第三種。
閱讀全文你將學習到:
更簡潔的深度克隆方式
Object.getOwnPropertyDescriptors()
api類型判斷的通用方法
問題分析
深拷貝 自然是 相對 淺拷貝 而言的。我們都知道 引用數據類型 變量存儲的是數據的引用,就是一個指向內存空間的指針, 所以如果我們像賦值簡單數據類型那樣的方式賦值的話,其實只能複製一個指針引用,並沒有實現真正的數據克隆。
通過這個例子很容易就能理解:
const obj1 = {
name: 'superman'
}
const obj2 = obj1;
obj1.name = '前端切圖仔';
console.log(obj2.name); // 前端切圖仔
複製代碼
所以深度克隆就是爲了解決引用數據類型不能被通過賦值的方式 複製 的問題。
引用數據類型
我們不妨來羅列一下引用數據類型都有哪些:
ES6 之前:Object, Array, Date, RegExp, Error,
ES6 之後:Map, Set, WeakMap, WeakSet,
所以,我們要深度克隆,就需要對數據進行遍歷並根據類型採取相應的克隆方式。當然因爲數據會存在多層嵌套的情況,採用遞歸是不錯的選擇。
簡單粗暴版本
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;
}
複製代碼
其實這就是我們最前面提到的第二種方式,很傻對不對,明眼人一眼就能看出來有很多冗餘代碼可以合併。
我們先進行最基本的優化:
合併冗餘代碼
將一眼就能看出來冗餘的代碼合併下。
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);
});
}
// 優化此部分冗餘判斷
// 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;
}
複製代碼
爲了驗證代碼的正確性,我們用下面這個數據驗證下:
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);
複製代碼
執行結果:
還有進一步優化的空間嗎?
答案當然是肯定的。
// 判斷類型的方法移到外部,避免遞歸過程中多次執行
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 的原型創建對象
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 {
// 基本數據類型和function
newObj[key] = val;
}
}
return newObj;
}
複製代碼
執行結果如下:
Object.getOwnPropertyDescriptors()
方法用來獲取一個對象的所有自身屬性的描述符。返回所指定對象的所有自身屬性的描述符,如果沒有任何自身屬性,則返回空對象。
具體解釋和內容見 MDN
這樣做的好處就是能夠提前定義好最後返回的數據類型。
這個實現參考了網上一位大佬的實現方式,個人覺得理解成本有點高,而且對數組類型的處理也不是特別優雅, 返回類數組。
我在我上面代碼的基礎上進行了改造,改造後的代碼如下:
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;
}
複製代碼
雖然代碼量上沒有什麼優勢,但是整體的理解成本和你清晰度上我覺得會更好一點。那麼你覺得呢?
最後,還有循環引用問題,避免出現無線循環的問題。
我們用 hash 來存儲已經加載過的對象,如果已經存在的對象,就直接返回。
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]);
}
}
} else {
res = obj;
}
hash.set(obj, res);
return res;
}
複製代碼
總結
對於深拷貝的實現,可能存在很多不同的實現方式,關鍵在於理解其原理,並能夠記住一種最容易理解和實現的方式,面對類似的問題才能做到 臨危不亂,泰然自若。上面的實現你覺得哪個更好呢?歡迎大佬們在評論區交流~
更文不易, 看完記得點個贊支持一下哦~ 這將是我寫作的動力源泉~
作者:前端 superman
鏈接:https://juejin.cn/post/7075351322014253064
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/KYW2UoJUVqGB2OCSF9vBFg