iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 12
1

拿到它,先看它是什麼?拿它來做什麼? 再決定。

在複製陣列之前,先理解了 JavaScript 會依資料型別 Primitive type(基本型別)和Non-primitive type(非基本型別)而有不同的運作方式,我們就可以進一步的來了解,我們常聽到的深拷貝(DeepCopy)和淺拷貝(shallow) 是什麼、在什麼情境下要選擇哪一種複製方法、如何實作出這些深拷貝和淺拷貝。

什麼是拷貝?

在生活中我們最常做的拷貝就是影印,假如我有一份筆記,朋友跟我借去拷貝,當他拿著我的筆記,到影印機前,按下拷貝鍵的那一刻,複製的筆記從機器跑出來,這時「拷貝」這件事就算完成,「我的筆記」和「他影印的筆記」是各自獨立的筆記,之後他要在這份影印的筆記上塗鴉也好、修改也好,也不會影響我的筆記。

但是在 JavaScript 裡,當我們新建立一個變數a,賦予這個變數一個值,然後再建立一個新變數b,接著以=指定運算子來把a 指定給b,這樣就算拷貝嗎?雖然我們ab叫出來看都是ˋ42ˋ,但是這真的是拷貝嗎。?是或不是?

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(呼叫變數的記憶體位址),也就是說當我們複製這類型的資料,只是複製了這個變數的記憶體位置,所以當我們呼叫複製和被複製的變數時,都會指向同一個記憶體位置,當然,裡面的值也是同一個值,改任何一個,都會動到兩個變數的值。
我們再來複習一下前一篇的範例:

// 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

let arr1 = [1,2,3];
let arr2 = arr1.slice(0);

arr2[0] = 42;
arr1; // [1, 2, 3]
arr2; // [42, 2, 3]

但是以下這個範例,arr1陣列裡的「元素」是「物件型別」的陣列,淺拷貝對於原物件裡面的元素值是物件型態就是不行! 噠美噠美

// 淺拷貝 []
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()| MDNJSON.parse() | MDN

JSON.stringify()方法是將一個 JavaScript 的值(物件或陣列)轉換為一個JSON的字串。

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]]"
接下來再轉回陣列的型態:

arr = JSON.parse(arr); // [[1,2,3],[4,5,6]]

把原本的物件值轉成字串,然後再轉回來物件的型態,兩個函式手牽手處理下來,就等於複製了一份arr1arr2

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)了。

優比~週末了!但是別人過週末,我們還是要過鋼鐵,明天繼續囉~

如有需要改進的地方,拜託懇求請告知,我會盡量快速度修改,感謝您~


上一篇
JS 以陣列 Array 的複製談型別(上)
下一篇
JS 遍歷陣列 Array 的方法
系列文
JavaScript之一定要了解的 Array 與方法34

1 則留言

0
Chris
iT邦新手 5 級 ‧ 2019-09-27 14:08:46

Google: 拷貝忍者卡卡西

tsuifei iT邦新手 5 級 ‧ 2019-09-27 17:03:31 檢舉

拷貝拷貝到天邊....

我要留言

立即登入留言