iT邦幫忙

2021 iThome 鐵人賽

DAY 20
1
Modern Web

舌尖上的JS系列 第 20

D20 - 濃濃咖啡香的深拷貝、淺拷貝

前言

什麼是拷貝? 今天朋友想 copy 你的報告,最簡單的就是影印一份給他,但是當你修改報告中的內容時,發現朋友拷貝的那份也跟著修改了,哪尼,難道我見證了量子糾纏?!

量子的世界請去找老高,但在 JavaScript 中,這個稱為 淺拷貝 的現象。

什麼是深拷貝、淺拷貝?

深拷貝與淺拷貝的概念像以下圖示:

淺拷貝:複製過後部分 A、B 的值還是會互相影響
深拷貝:複製過後 A、B 是完全的獨立個體,彼此值不影響

為什麼會有這兩種拷貝差異呢?

背後原理要先帶到 call by valuecall by sharing 觀念。

Call By Value & Call By Sharing

關於這部分有很多大神的文章可以拜讀,這裡簡單統整一下結論,JavaScript 中變數分為兩種型別:

  • 基本型別:複製變數時,記憶體中會新增一個擁有同樣值的新記憶體位置,呼叫變數會丟出在記憶體中存取的值,稱為 call by value。

  • 物件型別:複製物件型別的變數時,實際上是複製記憶體中的地址,相同的記憶體位置指向一樣的值; 對物件賦予新的值,則記憶體存的值也會跟著變動為新位置的地址,稱為 call by sharing。

原始型別沒有深淺拷貝之分,主要是物件型別的變數因記憶體存址指向的關係,淺拷貝指向的記憶體位置相同,而深拷貝指向不同的記憶體位置,因此有修改資料會不會影響的差異。


接下來透過程式碼,針對不同的物件拷貝行為來看看是屬於淺拷貝還是深拷貝!
舉例: A、B、C 三人都點了拿鐵,但在尺寸及其他項目上有不同的要求

淺拷貝

1. 變數賦值

使用一般的 = 賦值方式

let A = {coffee: 'Latte', size: 'L'};
let B = A;     // A 賦值給 B
B.size = 'M'   // 修改 B 的資料

console.log(B)  // {coffee: 'Latte', size: 'M'}
console.log(A)  // {coffee: 'Latte', size: 'M'},A咖啡尺寸從 L 變成 M

2. Object.assign

Object.assign 可以複製物件中的資料到另一個物件上,當物件資料只有一層時,可以做到資料不互相影響,但若結構來到兩層以上時,資料還是會受彼此影響,依然屬於淺拷貝。

let A = {coffee: 'Latte', size: 'L'};
let B = Object.assign({}, A);
B.size = 'M'    // b 更改尺寸為 M

console.log(B)   // {coffee: 'Latte', size: 'M'}
console.log(A)   // {coffee: 'Latte', size: 'L'},A 資料不受影響

// B 複製給 C
B.others = {sugar: 'less', shot: 3}
let C = Object.assign({},B)
C.size = 'S'
C.others.sugar = 'double'
C.others.shot = 1

console.log(C)  // {coffee: 'Latte', size: 'S', others: {sugar: 'double', shot: 1}}
console.log(B)  // {coffee: 'Latte', size: 'M', others: {sugar: 'double', shot: 1}}

// B 的 size 屬於第一層資料,不受 C 影響,但是第二層的 others 資料會跟著變動

3. 展開運算子

使用 展開運算子 spread operator ... , 將物件的值存入另一個物件中,但跟 Object.assign 一樣問題,當資料來到兩層以上時還是淺拷貝。

let A = {coffee: 'Latte', size: 'L'};
let B = {...A}
B.size = 'M'    // 修改 B 的內容
B.others = {sugar: 'less', shot:3}  // 增加 B 的內容

console.log(A)  // {coffee: 'Latte', size: 'L'},不受影響
console.log(B)  // {coffee: 'Latte', size: 'M', others: {sugar: 'less', shot: 1}}


// 將有兩層資料的 B 同樣以解構賦值方式拷貝給 C
let C = {...B}
C.size = 'S'    // 修改 C 內容
C.others.sugar = 'less'
C.others.shot = 1

console.log(C)      // {coffee: 'Latte', size: 'S', others: {sugar: 'less', shot: 1}}
console.log(B)      // {coffee: 'Latte', size: 'M', others: {sugar: 'less', shot: 1}}

// B 的 size 屬於第一層資料,不受 C 影響,但是第二層的 others 資料會跟著變動

深拷貝

好的,上述的展開運算子和 Object.assign 頂多可以做到一層資料的完全拷貝(如: A、B),但在巢狀的資料結構,如 B、C 兩人的咖啡資料怎麼做到深拷貝呢? 透過 JSON 格式!

JSON.stringify 把物件轉成字串,再用 JSON.parse 把字串轉回為物件。

這是真正可以做到深拷貝的方法,但是僅限 JSON 格式!

// A、B 的拷貝沒問題了,直接從 B、C 拷貝試試
let B = {
coffee: 'Latte', 
size: 'L',
others: {sugar: 'less', shot: 3}
};

let C = JSON.parse(JSON.stringify(B))
C.size = 'S'
C.others.sugar = 'double'
C.others.shot = 1

console.log(B)  // {coffee: 'Latte', size: 'L',others: {sugar: 'less', shot: 3}};
console.log(C)  // {coffee: 'Latte', size: 'S',others: {sugar: 'double', shot: 1}};

// C 修改的內容並沒有影響到 B,這才做到真正的深拷貝

Reference

How to differentiate between deep and shallow copies in JavaScript
深入探討 JavaScript 中的參數傳遞:call by value 還是 reference?
JS 變數傳遞探討:pass by value 、 pass by reference 還是 pass by sharing?
JavaScript 淺拷貝 (Shallow Copy) 與深拷貝 (Deep Copy)


上一篇
D19 - 今晚我想來點 唯獨派 getter 唯寫派 setter
下一篇
D21 - 走!去瀏覽器吃 好味雙響 BOM DOM 飯
系列文
舌尖上的JS30

尚未有邦友留言

立即登入留言