本文總結了一些解決深拷貝的常用方法,沒有銀彈,每種方法都有其優劣,使用時要區分其場景。
在 JavaScript 中,深拷貝已經是一個老生常談的問題,也無數次被面試官用來考核一個前端的 JS 水平,同時,在開發中,如果對深拷貝理解不夠深刻,也會出現很難發現的 BUG。此前,我已經寫過一篇文章《深入理解 JavaScript 之克隆》,其中分析了深拷貝的概念,但是其解決方法還很粗淺,這篇文章,記錄了一些解決深拷貝的常用方法,沒有銀彈,每種方法都有其優劣,使用時要區分其場景。
递归拷贝#
递归拷贝應該是最開始了解深拷貝時遇到的解決辦法,其原理是對引用類型進行遍歷,判斷其值是否為引用類型,如果為引用類型則遞歸調用該函數並傳入該值,否則為簡單數據類型則執行常規賦值拷貝。
function deepClone(obj) {
var result;
if (!(obj instanceof Object)) {
return obj;
} else if (obj instanceof Function) {
result = eval(obj.toString());
} else {
result = Array.isArray(obj) ? [] : {};
}
for (let key in obj) {
result[key] = deepClone(obj[key]);
}
return result;
}
這是一個簡單的深拷貝方法,處理了對象、數組和函數,但是 Date、Regexp、Map 等引用類型都沒有處理,而且無法解決對象循環引用的場景。
JSON.parse(JSON.stringify(object))#
深拷貝一般用 JSON.parse (JSON.stringify (object)) 就可以解決了,
這種方法的局限性:
- 會忽略 undefined
- 不能序列化函數
- 不能解決循環引用的對象
undefined 和函數會被忽略,而嘗試拷貝循環引用的對象則會報錯:
Uncaught TypeError:Converting circular structure to JSON
另外,諸如 Map, Set, RegExp, Date, ArrayBuffer 和其他內置類型在進行序列化時會丟失。
但是在通常情況下,複雜數據都是可以序列化的,所以這個函數可以解決大部分問題,並且該函數是內置函數中處理深拷貝性能最快的。
Structured Clone 结构化克隆算法#
Structured cloning 是一種現有的算法,用於將值從一個地方轉移到另一地方。例如,每當您調用 postMessage 將消息發送到另一個窗口或 WebWorker 時,都会使用它。關於結構化克隆的好處在於它處理循環對象並支持大量的內置類型。結構化克隆包含使用 MessageChannel 和 History API。
MessageChannel#
MessageChannel API 允許我們創建一個新的消息通道,並通過它的兩個 MessagePort 屬性發送數據。
我們可以創建一個 MessageChannel 並發送消息。在接收端,消息包含我們原始數據對象的結構化克隆。
MessageChannel 的 postMessage 傳遞的數據也是深拷貝的,這和 web worker 的 postMessage 一樣。而且還可以拷貝 undefined 和循環引用的對象。
// 有undefined + 循環引用
let obj = {
a: 1,
b: {
c: 2,
d: 3
},
f: undefined
};
obj.c = obj.b;
obj.e = obj.a;
obj.b.c = obj.c;
obj.b.d = obj.b;
obj.b.e = obj.b.c;
function deepCopy(obj) {
return new Promise(resolve => {
const { port1, port2 } = new MessageChannel();
port2.onmessage = ev => resolve(ev.data);
port1.postMessage(obj);
});
}
// MessageChannel是異步的
deepCopy(obj).then(copy => {
let copyObj = copy;
console.log(copyObj, obj);
console.log(copyObj == obj);
});
但拷貝有函數的對象時,還是會報錯:
Uncaught (in promise) DOMException:Failed to execute 'postMessage' on 'MessagePort': function() {} could not be cloned.
這種方法的缺點是它是異步的,但是你可以使用 await 來解決。
const clone = await structuralClone(obj);
History#
如果你曾經使用 history.pushState () 寫過 SPA,你可以提供一個狀態對象來保存 URL。事實證明,這個狀態對象使用結構化克隆,而且是同步的。我們必須小心使用,不要把原有的路由狀態搞亂了,所以我們需要在完成克隆之後恢復原始狀態。為了防止發生任何意外,請使用 history.replaceState () 而不是 history.pushState ()。
const structuralClone = obj => {
const oldState = history.state;
history.replaceState(obj, document.title);
const copy = history.state;
history.replaceState(oldState, document.title);
return copy;
};
此方法的優點是:能解決循環對象的問題,並且支持許多內置類型的克隆,也是同步的。缺點是有的瀏覽器對調用頻率有限制。比如 Safari 30 秒內只允許調用 100 次
Notification#
Notification 是用於桌面通知的。也可以用來實現 JS 對象的深拷貝。
function structuralClone(obj) {
return new Notification('', {data: obj, silent: true}).data;
}
const obj = /* ... */;
const clone = structuralClone(obj);
此方法的優點就是可以解決循環對象問題,也支持許多內置類型的克隆,並且是同步的。缺點就是這個 api 的使用需要向用戶請求權限,但是用在這裡克隆數據的時候,不經用戶授權也可以使用。在 http 協議的情況下會提示你再 https 的場景下使用。
由於某種原因,Safari 總是返回 undefined。
lodash 的深拷貝函數#
lodash 的_.cloneDeep () 支持循環對象,和大量的內置類型,對很多細節都處理的比較不錯,推薦使用。
解決循環引用#
通過分析上述的方法,發現只有結構化克隆算法才能解決循環引用的問題,那麼使用原生 JS 如何解決呢?這裡嘗試使用閉包來解決循環問題。
function deepClone(object) {
const memo = {};
function clone(obj) {
var result;
if (!(obj instanceof Object)) {
return obj;
} else if (obj instanceof Function) {
result = eval(obj.toString());
} else {
result = Array.isArray(obj) ? [] : {};
}
for (let key in obj) {
if (memo[obj[key]]) {
result[key] = memo[obj[key]];
} else {
memo[obj[key]] = obj[key];
result[key] = clone(obj[key]);
}
}
return result;
}
return clone(object);
}
var obj = {};
var b = { obj };
obj.b = b;
var obj2 = deepClone(obj);
在上述代碼中,對遞歸拷貝進行了優化,使用閉包來解決循環拷貝的問題,使用了 memo 這個變量來記錄被拷貝的引用地址,在每次遞歸前,記錄遞歸的參數值,這樣在遞歸內部就可以跳出遞歸,解決循環引用問題。
總結#
- 如果您沒有循環對象,並且不需要保留內置類型,則可以使用跨瀏覽器的 JSON.parse (JSON.stringify ()) 獲得最快的克隆性能。
- 如果你想要一個適當的結構化克隆,MessageChannel 是你唯一可靠的跨瀏覽器的選擇。