[JavaScript] 實作技巧: 淺拷貝(Shallow Copy) & 深拷貝(Deep Copy)
還記得我上一篇文章 [JavaScript] JavaScript 重要觀念: By Value & By Reference 嗎?
建議在理解 淺拷貝(Shallow Copy) & 深拷貝(Deep Copy)
時,最好要先建立 By Value & By Reference
的觀念
你才會知道為什麼要去探討 淺拷貝(Shallow Copy) & 深拷貝(Deep Copy)
這個議題
在 [JavaScript] JavaScript 重要觀念: By Value & By Reference 時,我們在說明時有提到一個例子:
let a = {
id: 0
}
let b = a
a.id = 2
console.log(b.id) // 會回傳的結果會是 0 還是 2 呢 ?
答案的結果為 2
, 那是因為 a 與 b 關聯的是同一個物件,但在撰寫 JavaScript 代碼往往並不是我們想要的結果
我們希望在 修改 a 物件底下的屬性時,不會去修改 b 物件底下的屬性值
這就是我們需要使用
淺拷貝(Shallow Copy) & 深拷貝(Deep Copy)
的實際需求
為什麼需要拷貝物件?
首先稍微小提一下,JavaScript 一個神奇的地方
{} === {} // return false
很神奇吧,明明一樣的 物件實字
,但兩者卻不相等,不過先不用詳細去了解其原因
你只要知道,這兩個 物件實字
,其實是 存放於兩個不同的記憶體空間位置
所以這兩者其實是不會互相影響的
同理的,我們在回到上面 a 與 b 的例子,如果我們要讓 a 與 b 的物件不被互相干擾,我們可以這麼做:
let a = {
id: 0
}
let b = {
id: 0
}
a.id = 2
console.log(b.id) // 會回傳的結果會是 0 還是 2 呢 ?
這是候,b.id
值,就是我們預期的 0
了,這也就是我們需要達到執行結果
但在實作上我們沒有辦法照上面的方式實作,所以我們需要透過 拷貝 的方式來回傳一個全新的物件,來達到上方的執行效果
如何拷貝物件:
其實在 ES6 推出後,我們只要使用 Object.assign()
的靜態方法,就可以進行實作
Object.assign()
會將一個或多個物件進行合併,並回傳一個 全新的物件參考
範例如下:
let a = {
id: 0
}
let b = Object.assign({}, a)
a.id = 2
console.log(b.id) // 會回傳的結果會是 0 還是 2 呢 ?
在這裡,回傳的結果就會是我們預期的 0
了, b 並不會因為 a 的修改而改動
好的,再來我們開始來探討何謂 淺拷貝(Shallow Copy) & 深拷貝(Deep Copy)
MDN: Object.assign()
淺拷貝(Shallow Copy) & 深拷貝(Deep Copy)
首先你要知道 JavaScript 是以原型為基礎的物件導向設計,以我們常見的一個 Date
的物件來說,我們可以用以下方式來示意 原型鏈
null => Object.prototype => Function.prototype => Date
這個概念對 淺拷貝(Shallow Copy) & 深拷貝(Deep Copy)
有著直接的關係
如果你對 原型鏈 的概念不熟悉,可以參考 原型基礎物件導向
在上面我們使用的 Object.assign()
的代碼中,其實就是一個 淺拷貝(Shallow Copy)
的範例,我們使用下圖進行示意:
在圖中左方的 new list Head
就是我們 淺拷貝(Shallow Copy)
出來的新物件,也就是剛剛我們範例中的 b
而從圖中,可以清楚的理解 深拷貝(Deep Copy)
就是 拷貝了整個物件的原型鏈
好的問題又來了,為什麼我們不全部使用 淺拷貝(Shallow Copy)
進行複製物件就好?
我用下列的範例來進行說明:
let a = {
name: {
first: 'HUANG',
last: 'Roxas'
}
}
let b = Object.assign({}, a)
a.name.first = 'LAI'
console.log(b.name.first) // 回傳結果會是 HUANG 還是 LAI ?
你會發現即使我們 淺拷貝(Shallow Copy)
了 a 的物件,給 b , 結果 b 還是會被 a 影響的,回傳結果為 ‘LAI’
因為在 a 物件底下的 name
又獨立有了一個 物件
,我們進行 淺拷貝(Shallow Copy)
時,並沒有將此物件拷貝到並建立出新的關聯
這個物件的巢狀設計,是非常常見的,此時我們必須要使用 深拷貝(Deep Copy)
來解決此問題
範例如下:
let a = {
name: {
first: 'HUANG',
last: 'Roxas'
}
}
let b = JSON.parse(JSON.stringify(a))
a.name.first = 'LAI'
console.log(b.name.first) // 回傳結果會是 HUANG 還是 LAI ?
如此一來問題就解決了,我們直接利用 JSON.stringify()
把整個物件變成字串,再藉由 JSON.parse()
帶入 b
的變數,此方法可以 完全複製整個原型鏈
達到 深拷貝(Deep Copy)
, 來完成我們期望的需求
另外我們常使用的 jQuery 中的
$.extend
方法,與 lodash_.cloneDeep
方法,都可以用來實作深拷貝(Deep Copy)
, 來達到我們要的效果