拷貝的基本定義
一般而言,深淺拷貝主要用來形容JavaScript中,關於對象的復制的。特別值得注意的一點就是,數組在JavaScript中的類型是屬於Object。
淺拷貝即只復制對象的引用,所以副本最終也是指向父對象在堆內存中的對象,無論是副本還是父對象修改了這個對象,副本或者父對象都會因此發生同樣的改變;
而深拷貝則是直接復制父對象在堆內存中的對象,最終在堆內存中生成一個獨立的,與父對象無關的新對象。深拷貝的對象雖然與父對象無關,但是卻與父對象一致。當深拷貝完成之后,如果對父對象進行了改變,不會影響到深拷貝的副本,同理反之亦然。
數組的淺拷貝
關於數組的淺拷貝,首先我們需要嘮嗑一下Array類提供的API:concat、slice;這兩個方法都會返回一個新數組,所以很多人一開始都誤以為這是屬於深拷貝的,其實不然。
MDN中關於concat的描述非常清楚:concat
方法不會改變this
或任何作為參數提供的數組,而是返回一個淺拷貝,它包含與原始數組相結合的相同元素的副本。具體請看下面案例:
強調:
下面的例中,我們一直都會使用復雜類型來稱呼,並且在案例中,復雜類型其實我們都是使用了JS中的對象,此時有基本知識不牢固的同學就會納悶,為什么不直接說對象。請注意!JS中的復雜類型有兩種,一種是Object,一種是Array;實質上function也是復雜類型的一種,但是實質上Array和function都屬於Object!據此,在數組中如果元素是數組,那么跟元素是對象,在拷貝時都是一樣的,下面只展示了是對象時的案例,是礙於篇幅的問題,請各位轉換思路即可。
concat方法
當數組內的元素都是簡單類型時:
1 var arr1 = [0,1,2,3,4,5]; 2 var arr2 = [6,7,8,9,10]; 3 var arr = arr1.concat(arr2); 4 // 更改arr1 中索引位 0 的元素的值 5 arr1[0] = 5; 6 console.log(arr);//輸出結果: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
可以看到,這種情況下活生生就是一個深拷貝的存在,別着急,我們繼續看下面的案例。
當數組內的元素有復雜類型時:
1 var arr3 = [{name:'xiaobai',age:18},10]; 2 var arr4 = [1,2,3]; 3 arr5 = arr3.concat(arr4); 4 //更改arr3 中索引值為 0 的對象的屬性 5 arr3[0].name = '小白'; 6 console.log(arr5);//輸出結果:[{name:'小白',age:18}, 10, 1, 2, 3]
此時就很清晰地可以看到,當數組內的對象發生改變的時候,使用concat合並的新數組同樣發生了改變。其實這並不難理解。
深淺拷貝一般都是用於描述復雜類型的復制的,基本類型的復制是在棧內存中生成一個一模一樣的值,父本和副本之間互相沒有任何關系。而復雜類型由於是通過在棧內存中的引用指向堆內存中的對象,所以根據復制棧內存的引用和復制堆內存的對象區分深淺拷貝。
slice方法
同樣的道理,我們在看看slice()方法,根據MDN的描述:slice()
方法返回一個新的數組對象,這一對象是一個由 begin
和 end
決定的原數組的淺拷貝(包括 begin
,不包括end
)。原始數組不會被改變。
同樣的,MDN中也描述了,如果元素是一個對象引用(不是實際對象),只會返回一個淺復制的新數組。我們通過案例來學習:
當數組內的元素是簡單類型時:
1 var arr1 = [0,1,2,3,4,5]; 2 var arr = arr1.slice(); 3 // 更改arr1 中索引位 0 的元素的值 4 arr1[0] = 5; 5 console.log(arr);//輸出結果: [0, 1, 2, 3, 4, 5]
這是因為對於字符串、數字及布爾值來說(不是 String
、Number
或者 Boolean
對象),slice
會拷貝這些值到新的數組里。在別的數組里修改這些字符串或數字或是布爾值,將不會影響另一個數組。
當數組內的元素是復雜類型時:
1 var arr3 = [{name:'xiaobai',age:18},10]; 2 arr5 = arr3.slice(); 3 //更改arr3 中索引值為 0 的對象的屬性 4 arr3[0].name = '小白'; 5 console.log(arr5);//輸出結果:[{name:'小白',age:18}]
這是因為如果該元素是個對象引用 (不是實際的對象),slice
會拷貝這個對象引用到新的數組里。兩個對象引用都引用了同一個對象。如果被引用的對象發生改變,則新的和原來的數組中的這個元素也會發生改變。
使用等號(=)實現數組的淺拷貝
使用等號實現數組的淺拷貝就不再過多言語了,如果數組的元素是基本數據類型,那么會直接克隆該元素到新數組,如果元素是復雜類型時,克隆的則是復雜類型的指向,這種情況下,無論是新數組還是舊數組,改變了復雜類型,兩個數組都會受到一樣的影響。
數組/對象的深拷貝
js數組中實現深拷貝的方法有很多中,比如JSON.parse(JSON.stringify())、遞歸以及jQuery庫的extend方法,都是可以實現數組和對象的深拷貝的。
但是仔細去品味,你會發現實質數組的深拷貝也是針對的復雜類型而言的,所以我們實現了數組的深拷貝,就意味着我們同時也可以實現復雜類型的深拷貝!據此我們同時把數組和對象的深拷貝放在一起演示。
JSON.parse(JSON.stringify())深拷貝
首先我們看一下,這種方式下復制的對象之間是一種怎樣的關系:
1 var obj = { 2 name: 'xiaobai', 3 age: 18 4 } 5 6 var copy = JSON.parse(JSON.stringify(obj)); 7 console.log(obj);//輸出結果:{name: "xiaobai", age: 18} 8 console.log(copy);//輸出結果:{name: "xiaobai", age: 18} 9 console.log(obj === copy);//輸出結果:false
可以看到,對於復雜類型的對象,使用JSON先序列化然后再反序列化,得到的結果雖然一致,但是變量 obj 和 copy 所指向的對象不是同一個,他們之間的比較結果是false,這種情況下兩個對象之一發生的任何變化都不會影響到其他一個。此時我們就實現了復雜類型的深拷貝了!但是千萬注意!JSON.Stringtify這個方法不能深拷貝函數!!這個要記得!
基於數組的深拷貝其實就是針對數組中的復雜類型的深拷貝,所以我們對整個數組使用JSON.parse(JSON.stringify())深拷貝即可。
遞歸深拷貝
在使用for...in 來實現深拷貝的時候需要注意一個坑,即原型鏈上的可枚舉屬性會被遍歷出來,這是我們所不希望看到的,所以進入正題之前首先說一下如何避免遍歷到原型鏈上的可枚舉屬性:
1 var obj = { 2 name: 'xiaohei', 3 age: 18 4 } 5 //在原型鏈上添加一個自定義的可枚舉屬性 6 obj.__proto__.eat = 'hello'; 7 8 for(var key in obj){ 9 console.log(key); 10 //遍歷結果:name age eat 11 }
可以看到,原型鏈上的可枚舉屬性也被遍歷出來了,為了避免出現這樣的情況,我們可以使用js提供的一個方法hasOwnProperty(),該方法會判斷當前屬性是自身屬性還是繼承而來的,或者說是自身屬性還是原型鏈上的可枚舉屬性,返回值使boolean,自身屬性為true,原型鏈可枚舉屬性為false。
1 var obj = { 2 name: 'xiaohei', 3 age: 18 4 } 5 //在原型鏈上添加一個自定義的可枚舉屬性 6 obj.__proto__.eat = 'hello'; 7 8 for(var key in obj){ 9 if(obj.hasOwnProperty(key)){ 10 console.log(key); 11 //輸出結果: name age 12 } 13 }
OK,進入正題,編寫一個遞歸方法,用來深拷貝復雜類型:
1
/** 2 * 3 * @param {Object} obj 傳遞進去進行遞歸的數組或者對象 4 */ 5 function deepClone(obj){ if(obj === null) return null; if(typeof obj !== 'object') return obj; if(obj instanceof RegExp) { return new RegExp(obj); } if(obj instanceof Date) { return new Date(obj); } /* 這里之所以使用constructor,是為了可以使的克隆出來的對象 與被克隆的對象都是同一個類的實例對象 */ let newObj = new obj.constructor; for(let key in obj) { if(obj.hosOwnProperty(key)) { newObj[key] = deepClone(obj[key]); } } return newObj; } 24 25 var arr = [1, 2, { name: 'xiaohei', age: 18 }]; 26 var newArr = deepClone(arr); 27 //修改父數組中對象元素的屬性值 28 arr[2].name = '小黑'; 29 console.log(arr); 30 console.log(newArr); 31 console.log(arr === newArr);
最終輸出的結果如下: