[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) , 來達到我們要的效果

Facebook 功能: