我在JavaScript中如何拷貝一個物件?這是一個簡單的問題,但是答案確不是很簡單。
Did you ever wanted to create a deep copy of an object in JavaScript? There is a way, but you are not gonna like it... I feel like we need something better ? pic.twitter.com/IDazhB8BKJ — Surma (@dassurma) 2018年1月22日
引用呼叫
JavaScript通過引用來傳遞所有的值。如果你不知道這是什麼意思,下面有個例子?:
function mutate(obj) {
obj.a = true;
}
const obj = {a: false};
mutate(obj)
console.log(obj.a); // prints true
複製程式碼
mutate
方法改變了作為引數傳遞進來的這個物件。在值呼叫
環境中,這個函式式傳遞的這個值,所以相關於這個函式是執行了一個拷貝。這個函式使這個物件對外是不可見的。但是在像js的這種引用呼叫
的環境,將會得到這個真實的物件。所以最後控制檯輸出的為true
。
不過,你想要保持你的原始的物件,其他函式只是建立了這個物件的拷貝。
在下面就介紹幾種深度拷貝的方式
JSON.parse
第一種最古老的方式就是通過將物件轉換為JSON字串格式,然後將其轉換為物件。
let obj = { name : "huyue" };
let copy = JSON.parse(JSON.stringify(obj));
obj.name = 'hy';
console.log(copy);//'huyue'
複製程式碼
但是這種方式有些問題
問題一:當物件中出現迴圈引用的時候會報錯。儘管你可能認為你不會如此使用,但是那些還是會很容易發生。比如當你構建了樹狀型別的資料機構的時候,其中一個節點引用了父級的某個節點,這樣就出現了這種場景。
const x = {};
const y = {x};
x.y = y; // Cycle: x.y.x.y.x.y.x.y.x...
const copy = JSON.parse(JSON.stringify(x)); // throws!
複製程式碼
問題二:這種方式只支援基礎型別,像Map,Set,RegExp,Date,ArrayBuffer,函式物件等都會在序列化的時候弄丟
var source = { name:function(){console.log(1);}, child:{ name:"child" } }
var target = JSON.parse(JSON.stringify(source));
console.log(target.name); //undefined
複製程式碼
注:JSON物件是ES5中引入的新的型別(支援的瀏覽器為IE8+),瀏覽器支援情況
結構化克隆
結構化克隆是一個現有演算法,它是被用來把一個領域的值傳遞到另一個。比如,你呼叫postMessage去傳送一個訊息給另一個視窗或WebWorker。結構化很好的地方就是他能處理迴圈物件,並且支援多種內建型別。
MessageChannel
我們通過MessageChannel建立一個新的訊息通道,並通過它的兩個MessagePort屬性來傳送資料和獲取資料。我們接受到的這條資訊就是會包含原始資料的結構化克隆物件。但是這種方式是非同步情況,所以下面例子使用的async awit實現了的,也可參見線上地址
function structuralClone(obj) {
return new Promise(resolve => {
const {port1, port2} = new MessageChannel();
port2.onmessage = ev => resolve(ev.data);
port1.postMessage(obj);
});
}
const obj = /* ... */;
const clone = await structuralClone(obj);
複製程式碼
注:瀏覽器支援IE10+,瀏覽器支援力度情況
History API
如果你曾經使用過history.pushState()
去構建一個SPA(單頁應用),你應該會知道能提供一個狀態物件去儲存這URL。這個狀態物件就是結構化克隆,並且還是同步的。我們一定要小心,避免在使用這個狀態物件的時候去混淆任何程式邏輯,所以我們需要在我們克隆了之後去恢復這個原始的狀態物件。為了防止發生任何事件,請使用history.replaceState()而不是history.pushState()。replaceState和pushState區別詳情
function structuralClone(obj) {
const oldState = history.state;
history.replaceState(obj, document.title);
const copy = history.state;
history.replaceState(oldState, document.title);//就是為了恢復原始狀態物件,避免干擾
return copy;
}
const obj = /* ... */;
const clone = structuralClone(obj);
複製程式碼
為了複製一個物件,使用瀏覽器的引擎感覺有些笨拙。不過你還是可以這麼做,有些事情還是得注意,因為Safari瀏覽器會限制30秒內呼叫relaceState的次數上限為100次
注:瀏覽器支援IE10+,瀏覽器支援力度情況
Notification API
這種方式由Jeremy Banks建議,通知介面用於向使用者配置和顯示桌面通知,這個訊息通知的api有一個與它們相關的資料物件被克隆。看到這,可能有的人表示有點不是很明白,那麼可以點選線上示例
function structuralClone(obj) {
return new Notification('', {data: obj, silent: true}).data;
}
const obj = /* ... */;
const clone = structuralClone(obj);
複製程式碼
它基本觸犯了瀏覽器內的許可權機制,所以懷疑這個可能會非常慢。出於某種原因,Safari瀏覽器總是返回undefined
。可以使用線上示例
注:瀏覽器不支援IE,瀏覽器支援力度情況
效能測試
對上面幾種方式進行效能測試看哪種方式效能最高。剛開始嘗試時,我拿一個小JSON物件,並通過這些克隆物件一千次的不同方式來進行測試。幸運的是, Mathias Bynens告訴我在給一個物件增加屬性的時候V8是有快取。為了確保不走快取,所以我寫了一個[函式](a function that generates objects of given depth and width using random key names),使用隨機鍵名稱生成給定深度和寬度的物件,並重新執行測試示例
圖表統計
總結
- 如果你不會使用迴圈物件並且不會使用內建型別,那麼還是推薦使用JSON.parse。並且瀏覽器相容性還更好(ie8+)
- 如果在考慮效能和瀏覽器相容,
MessageChannel
是最好的選擇。(ie10+)