# 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)了。
優比~週末了!但是別人過週末,我們還是要過鋼鐵,明天繼續囉~
> 如有需要改進的地方,拜託懇求請告知,我會盡量快速度修改,感謝您~