# 12_以陣列 Array 的複製談型別_下 ###### tags: `鐵人賽 ` ###### Day 12 > 拿到它,先看它是什麼?拿它來做什麼? 再決定。 在複製陣列之前,先理解了 JavaScript 會依資料型別 Primitive type(基本型別)和Non-primitive type(非基本型別)而有不同的運作方式,我們就可以進一步的來了解,我們常聽到的深拷貝(DeepCopy)和淺拷貝(shallow) 是什麼、在什麼情境下要選擇哪一種複製方法、如何實作出這些深拷貝和淺拷貝。 ## 什麼是拷貝? 在生活中我們最常做的拷貝就是影印,假如我有一份筆記,朋友跟我借去拷貝,當他拿著我的筆記,到影印機前,按下拷貝鍵的那一刻,複製的筆記從機器跑出來,這時「拷貝」這件事就算完成,「我的筆記」和「他影印的筆記」是各自獨立的筆記,之後他要在這份影印的筆記上塗鴉也好、修改也好,也不會影響我的筆記。 但是在 JavaScript 裡,當我們新建立一個變數`a`,賦予這個變數一個值,然後再建立一個新變數`b`,接著以`=`指定運算子來把`a` 指定給`b`,這樣就算拷貝嗎?雖然我們`a`、`b`叫出來看都是ˋ42ˋ,但是這真的是拷貝嗎。?是或不是? ```javascript let a = 42; let b = a; a; // 42 b; // 42 ``` ### 被複製的資料型別是重點 事實上,這得要看我們要複製什麼東西,才能知道我們是否可以「輕易」的複製,且「真正」的把複製和被複製的徹底分離。 更確切的說,如果我們要複製的變數值型態,是屬於Primitive data types(基本資料型別),也就是以下的資料型態: - Number (數字),例如 `42` - String (字串),例如 `"Hi"` - Boolean (布林值),例如 `true` - undefined - null 那麼我們就可以放心的以上述的方式複製。 還記得前一篇提到的 Call by value(呼叫變數的值)嗎?這些資料型別在 JavaScript 裡是屬於 Immutable (不可變)的基本型別,以值的方式複製可以得到真正的、獨立的複製。 ### 你以為你複製了,但是並。沒。有。 但是,如果今天要被複製的資料型態是 Non Primitive data types(非基本資料型別),也就是 Object (物件型別),那就無法完全複製。例如: - Array (陣列),例如 `[1, 2, 3, 4, 5]` - Object (物件),例如 `{ name : "Tsuifei"}` 因為陣列和物件是屬於 Call by reference(呼叫變數的記憶體位址),也就是說當我們複製這類型的資料,只是複製了這個變數的記憶體位置,所以當我們呼叫複製和被複製的變數時,都會指向同一個記憶體位置,當然,裡面的值也是同一個值,改任何一個,都會動到兩個變數的值。 我們再來複習一下前一篇的範例: ```javascript // by reference(參考值) let person = { name: "Tracy", city: "Tainan" } let person2 = person; person2.name = "Ayda" person; // name: "Ayda" person2; // name: "Ayda" ``` 我們可以看到,在我們修改從`person`複製出來的`person2`時,`person`也被修改了。 ## 物件專用的深拷貝和淺拷貝 終於,我們要進入 ~~深眠和淺眠~~ 這個正題了。 不知大家有沒發現,在討論這個深淺拷貝的範例時,清一色都是用物件來示範?原來,「深拷貝」和「淺拷貝」是針對物件的資料型別複製時,所產生的現象而來的啊! 但是要如何在 JavaScript 中區分深拷貝和淺拷貝?何時該用「深拷貝」或「淺拷貝」,用最簡單的方式是取決於我們想要複製的資料`[元素]`是什麼型別。 ~~結束。~~ ### 淺拷貝 [ ] 只要一層都好說 完全的複製 Array 而不受原陣列影響,即使修改複製過來的物件裡的值,也不會改變複製來源,這個物件裡面的「元素」可以是任何一種資料型態,反著說,就是這個物件裡面的元素,不能是物件。如果遇到這樣的資料,就可以用淺拷貝的方法複製。 有哪幾種方法可以做淺拷貝?最常被拿來用的是 JavaScript 內建的陣列方法`slice()`。 ~~它的詳細解說會在後幾章才會介紹到。~~ `slice()`通常拿來做從陣列中切取我們需要的元素出來,在這裡我們使用`slice(0)`表示我們要從頭到尾都切下來。 ~~切切切~~ 在下面第一個範例,「淺拷貝」是可行的,因為`arr1`陣列裡的元素是基本資料型別`Number` ```javascript let arr1 = [1,2,3]; let arr2 = arr1.slice(0); arr2[0] = 42; arr1; // [1, 2, 3] arr2; // [42, 2, 3] ``` 但是以下這個範例,`arr1`陣列裡的「元素」是「物件型別」的陣列,淺拷貝對於原物件裡面的元素值是物件型態就是不行! ~~噠美噠美~~ ```javascript // 淺拷貝 [] let arr1 = [[1,2,3],[4,5,6]]; let arr2 = arr1.slice(0); arr2[0][0] = 42; arr1; // 0: (3) [42, 2, 3] // 1: (3) [4, 5, 6] arr2; // 0: (3) [42, 2, 3] // 1: (3) [4, 5, 6] ``` 網路上能找到的大多是淺拷貝的例子,雖然淺 ~~可別因此就鄙視它~~,只要確認要複製的來源物件,裡面的元素不是物件型別,還是非常好用的。 礙於篇幅,這裡只介紹一種淺拷貝的方式,有興趣的朋友可查找網路上其他的方法,例如用解構式、迴圈或使用`map()`都可達到淺拷貝的效果。 ### 深拷貝 [ [ ],[ ] ] --> DNA被複製,桃莉羊出現了 何時使用深拷貝?當我們想要完全複製一份「物件」裡面的元素也是「物件」,就可以使用深拷貝,這種情境就是我們在本文開頭所說的,用影印機複製筆記ㄧ樣,複製完就是兩個獨立的個體了。 做深拷貝的方法並不多,大部分都是靠外來的函式庫來撐腰,例如 lodash 和 jQuery 的第三方主流函式庫,如果使用原生的 JavaScript 來做深拷貝,似乎只能使用`JSON.stringify()`和`JSON.parse()`的交互作用,達到深拷貝的效果。 來看一下 MDN 對這兩個函式的解釋: [JSON.stringify()| MDN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) | [JSON.parse() | MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse) > `JSON.stringify()`方法是將一個 JavaScript 的值(物件或陣列)轉換為一個`JSON`的字串。 ```javascript let arr = [[1,2,3],[4,5,6]]; arr = JSON.stringify(arr); // "[[1,2,3],[4,5,6]]" ``` > `JSON.parse()`方法用來解析`JSON`的字串,構造由字串描述的JavaScript值或物件。 這時的`arr`已經變成JSON的字串格式:`"[[1,2,3],[4,5,6]]"`。 接下來再轉回陣列的型態: ```javascript arr = JSON.parse(arr); // [[1,2,3],[4,5,6]] ``` 把原本的物件值轉成字串,然後再轉回來物件的型態,兩個函式手牽手處理下來,就等於複製了一份`arr1`到`arr2`。 ```javascript function jsonDeepClone(obj) { return JSON.parse(JSON.stringify(obj)); } let arr1 = [[1,2,3],[4,5,6]]; let arr2 = jsonDeepClone(arr1); arr2[0][0]=42; arr1; // 0: (3) [1, 2, 3] // 1: (3) [4, 5, 6] arr2; // 0: (3) [42, 2, 3] // 1: (3) [4, 5, 6] ``` 這樣的一個拷貝過程與結果就是深拷貝(DeepCopy)了。 優比~週末了!但是別人過週末,我們還是要過鋼鐵,明天繼續囉~ > 如有需要改進的地方,拜託懇求請告知,我會盡量快速度修改,感謝您~