# 為什麼我們需要區分深、淺拷貝? 在寫程式的過程中,我們經常遇到因「深、淺拷貝」問題而導致操作上誤改其他數值的問題。你或許會好奇,程式語言的設計者為什麼要這麼「麻煩」?為什麼會需要區分「深、淺拷貝」呢? > 核心重點: > 1. JavaScript 預設行為(淺拷貝)是**基於效率**,僅**複製物件第一層的基本型別的值**或**物件的引用**。 > 2. 這種引用複製導致巢狀物件共享記憶體,可能造成意外修改,因此需要**深拷貝**來確保資料獨立性。 > 3. 深拷貝是**為了解決日益複雜的數據操作需求**,隨著語言發展而提供的額外機制(如 `structuredClone()`)。 --- ## 「傳值」與「傳引用」:核心概念 要理解深、淺拷貝,首先得回顧 **「傳值」(Pass by Value)與「傳引用」(Pass by Reference)** 這兩個經典的概念。在 JavaScript 中,變數宣告和賦值的行為,就依循著這兩種方式: * **基本型別 (Primitive Types) 是「傳值」:** 複製像數字、字串、布林值等基本型別時,變數會直接儲存這些值本身。 ```javascript! // 變數 a 指向記憶體某處,儲存著數值 1 let a = 1; // 變數 b 宣告時,a 的值 (1) 被複製到 b 的記憶體空間 // 現在 a 和 b 各自獨立地儲存著數值 1,互不影響 let b = a; ``` * **物件型別 (Object Types) 是「傳引用」:** 複製物件、陣列等物件型別時,變數儲存的並非物件的實際內容,而是它在記憶體中的 **「引用」(或稱記憶體指標,pointer)**。因此,賦值時複製的是這個引用。 ```JavaScript! // 變數 a 指向記憶體某處,該處儲存著物件 { number: 1 } 的「記憶體位址」(例如 0x0001) let a = { number: 1 }; // 變數 b 宣告時,a 所儲存的「記憶體位址 0x0001」被複製給 b // 現在 a 和 b 都儲存著 0x0001,因此它們都指向了記憶體中的同一個物件 let b = a; // 如果 b.number 被修改了,a.number 也會跟著改變,因為它們指向的是同一個物件 ``` ![Frame 16](https://hackmd.io/_uploads/BkeEQnc3Ulg.png) ## 「淺拷貝」:預設的複製行為 當我們討論「拷貝」物件時,程式語言內建或常見的複製方法,通常應用的是**淺拷貝(Shallow Copy)**。 淺拷貝**只會複製物件的第一層屬性**。如果屬性值是基本型別,它們會被獨立複製;但如果屬性值是巢狀物件(nested objects),淺拷貝只會複製這些巢狀物件的引用。 例如,當我們使用展開運算符 ... 來複製物件時: ```javascript! let a = { name: "Charlie", profile: { age: 18, gender: "male" }}; let b = { ...a }; // 這樣 b 會得到 a 的第一層屬性副本: // name 是一個基本型別,所以 b.name 是一個獨立的 "Charlie" 副本 // profile 是一個物件型別,所以 b.profile 複製的是 a.profile 的「記憶體位址」 // 這使得 b.profile 和 a.profile 仍然指向同一個巢狀物件 // 因此,如果 b.profile.age 被修改了,a.profile.age 也會跟著變動 ``` ![Frame 20](https://hackmd.io/_uploads/SJTX392Lee.png) 聽起來就是一個「傳值傳址判斷地獄!」因為你得要頭腦清楚知道現在複製的東西是什麼型別、展開到第幾層。然而,在 JavaScript 中,大多數原生的陣列和物件拷貝方法,如 `Array.from()`、`Array.prototype.slice()`、`Object.assign()`,以及展開運算符 `...`,都屬於淺拷貝。 ## 「深拷貝」:完全獨立的副本 然而實際開發上,淺拷貝的限制導致開發者會誤改資料。那如果需要一個完全獨立的物件副本,讓新舊物件互不影響,該怎麼辦呢?這就是 **深拷貝(Deep Copy)** 需要出場的時候。 實作深拷貝的方法通常有: * **`JSON.parse(JSON.stringify(obj))`:** 這是一個常見的取巧方法,將物件先轉換成 `JSON` 字串,再解析回物件。由於字串是基本型別,這過程會切斷所有引用關係。 ```JavaScript let b = JSON.parse(JSON.stringify(a)); ``` ![Frame 21](https://hackmd.io/_uploads/Bk443c28gx.png) 但這種方法有其侷限性。它無法正確處理函數、`undefined`、`Symbol`、`Date` 物件(會被轉成字串),以及物件中存在的循環引用(即物件內部互相指向的情況)。 * **`structuredClone()` (ES2022 新增):** 這是 JavaScript 近年來引入的原生深拷貝方法,功能更強大,能處理更多複雜型別並避免循環引用錯誤。 * **使用第三方函式庫:** 例如 Lodash 的 `_.cloneDeep()`,提供更全面的深拷貝功能。 * **手動迴圈拷貝:** 對於特定需求,也可以自行編寫迴圈來實現深拷貝。 ### 總結 深、淺拷貝的區分,與「傳值傳引用」的問題密切相關。 最主要的原因在於,程式語言(JavaScript)在設計之初,預設的「拷貝」行為是高效且直接的「淺拷貝」——它複製的是物件在記憶體中的「位址」。這樣能節省大量記憶體和計算資源,因為不需要為每個物件都創建一個完整的物理副本。 然而,隨著程式複雜度增加,當開發者需要**完全獨立的資料副本**,避免意外的副作用時,這種「引用拷貝」就顯得不足了。這使得「深拷貝」的需求逐漸浮現,並促使語言本身或其生態系發展出更全面、更便捷的深拷貝機制(例如 structuredClone() 的引入),以解決這個痛點。 所以,深、淺拷貝並非「麻煩的設計」,而是語言在**效率與精確控制**之間不斷權衡與演進的結果。 ### 參考資料 [MDN | Shallow copy](https://developer.mozilla.org/en-US/docs/Glossary/Shallow_copy) [MDN | Deep copy](https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy) [MDN | structuredClone](https://developer.mozilla.org/en-US/docs/Web/API/Window/structuredClone)