什麼是拷貝? 今天朋友想 copy 你的報告,最簡單的就是影印一份給他,但是當你修改報告中的內容時,發現朋友拷貝的那份也跟著修改了,哪尼,難道我見證了量子糾纏?!
量子的世界請去找老高,但在 JavaScript 中,這個稱為 淺拷貝 的現象。
深拷貝與淺拷貝的概念像以下圖示:
淺拷貝:複製過後部分 A、B 的值還是會互相影響
深拷貝:複製過後 A、B 是完全的獨立個體,彼此值不影響
為什麼會有這兩種拷貝差異呢?
背後原理要先帶到 call by value 與 call by sharing 觀念。
關於這部分有很多大神的文章可以拜讀,這裡簡單統整一下結論,JavaScript 中變數分為兩種型別:
基本型別:複製變數時,記憶體中會新增一個擁有同樣值的新記憶體位置,呼叫變數會丟出在記憶體中存取的值,稱為 call by value。
物件型別:複製物件型別的變數時,實際上是複製記憶體中的地址,相同的記憶體位置指向一樣的值; 對物件賦予新的值,則記憶體存的值也會跟著變動為新位置的地址,稱為 call by sharing。
原始型別沒有深淺拷貝之分,主要是物件型別的變數因記憶體存址指向的關係,淺拷貝指向的記憶體位置相同,而深拷貝指向不同的記憶體位置,因此有修改資料會不會影響的差異。
接下來透過程式碼,針對不同的物件拷貝行為來看看是屬於淺拷貝還是深拷貝!
舉例: A、B、C 三人都點了拿鐵,但在尺寸及其他項目上有不同的要求
使用一般的 =
賦值方式
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
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 資料會跟著變動
使用 展開運算子 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,這才做到真正的深拷貝
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)