複製陣列問題(Javascript)

複製陣列後修改新的陣列,結果舊的陣列也跟著被改動了,怎麼會這樣???

一開始遇到這樣的問題,還以為是什麼Bug在作亂

但冷靜思考過後,發現這是很基本但容易忽略的問題(但又不好解決)

那到底要如何複製陣列,又不影響原陣列呢?

首先回想一下,基本型別(Primitive Type)跟物件(Object)兩者有什麼差異呢

基本型別是Number,String,Boolean

(若是其他程式語言還會細分Int,Char,Long......等等)

而物件則包含自己透過function或用{}建立的物件(Object)

其中也包含我們今天的主角:陣列(Array)

 

而基本型別和物件最大的差異就是傳值的方法

基本型別是傳值(call by value)

而物件則是傳參考(call by reference)

這時要先來介紹一下記憶體的部分

記憶體通常會分成幾個部分

最常聽到的就是stack、heap、static等等

可以想成不同等級的房子,stack是最基本的、heap是比較高級的

所以會依照用途使用到不同的記憶體位置

基本型別會將值儲存在stack的位置

一個變數就給一個stack的位置

所以當第二個變數要參考到第一個變數時,就會直接找到stack的位置取走裡面的值

比如說

var a = 10;

var b = a;  //此時b取走a的值,所以b等於10

console.log(b);  //10

b = 100;  //在這裡我去修改b的值

console.log(a);  //a和b不相關,所以b的改動不會影響到a,故a=10

console.log(b);  //b=100

這就是所謂的傳值(call by value)

 

但物件會將資料儲存在heap位置

而將heap記憶體位置的地址儲存在stack裡

因此對下列程式碼來講

var arr = [0,1,2];

JS會將陣列資料[0,1,2]儲存在heap裡

而將記憶體位置儲存在stack區裡的變數arr

又或者像下列程式碼

var obj = new Object();

JS會在heap建立一個新的Object(但裡面是空的沒有值)

而將Object的記憶體位置的地址儲存在stack區裡的變數obj

因此

var arr1 = [0,1,2];

var arr2 = arr1;

console.log(arr2);  //[0,1,2]

第二行的程式碼實際上只是將arr1所儲存在stack裡的記憶體位置傳給arr2

這就是所謂的傳參(call by reference)

而等號 '=' 並不是讓兩個陣列相等,而是將arr1記憶體位置指派給arr2,這也是為什麼等號會是'指派'運算子

 

然而問題來了

var arr1 = [0,1,2];

var arr2 = arr1;

arr[1] = 9;

這樣會發生什麼事呢?

答案會是

console.log(arr1)  //[0,9,2]

console.log(arr2);  //[0,9,2]

原因是arr1和arr2這兩個變數只是儲存著陣列的記憶體位置

而這兩個變數所儲存的記憶體位置是相同的

代表兩個變數會參考到同一個陣列

所以arr2更改陣列的值時,也會影響到arr1所參考到的陣列(畢竟就同一個嘛)

 

那要如何複製陣列,修改時才不會影響到彼此呢?

首先先來討論Javascript陣列中的淺拷貝(Shallow Copy)與深拷貝(Deep Copy)

淺拷貝在複製時會參考到同一個物件,也就是複製記憶體位置

深拷貝則是會使用獨立的heap記憶體位置,複製成一個完全沒相關聯的新陣列

所以要深拷貝陣列,才能達到不影響彼此的情況

目前大多數複製陣列的方法都是淺拷貝

舉凡像是slice,map...等等

不過淺拷貝適用於一維陣列

條件是陣列裡的元素不能是物件

只要符合條件,使用淺拷貝也能達成新舊陣列不互相影響的狀況

舉例來說

var arr1 = [0,1,2];

var arr2 = arr1.slice(0);

arr2[1] = 9;

console.log(arr1);  //[0,1,2]

console.log(arr2);  //[0,9,2]

但如果陣列是多維的話,淺拷貝就不適用了

 

雖然在網路上看到很多深拷貝的方法

像是ES6的新函式Object.assign

或是jquery的$extend

但實際使用上卻都失敗

最後是使用JSON.stringify把陣列轉成字串

再用JSON.parse把字串轉成新的物件

畢竟換成字串之後就是新的東西啦,就沒有參考記憶體位置的狀況了

但這個方法有些缺點

最主要的就是物件格式要能轉成JSON

如果是function就沒辦法了

而且多了一道處理手續,多少會影響到效能

當然還有土法煉鋼,自己一個一個慢慢加,但陣列一大就很刺激了

 

可惜目前如果不使用第三方函式庫的話,好像也只有JSON轉換能做到深拷貝

而其他還有像lodash函式庫聽說也能達到,不過沒有實際去測試

如果以後有找到更好用的方法會再更新上來~~~~