通常而言,深淺拷貝主要用來形容JavaScript中,關於對象的複製的。特別值得注意的一點就是,數組在JavaScript中的類型是屬於Object。數組
淺拷貝即只複製對象的引用,因此副本最終也是指向父對象在堆內存中的對象,不管是副本仍是父對象修改了這個對象,副本或者父對象都會所以發生一樣的改變;學習
而深拷貝則是直接複製父對象在堆內存中的對象,最終在堆內存中生成一個獨立的,與父對象無關的新對象。深拷貝的對象雖然與父對象無關,可是卻與父對象一致。當深拷貝完成以後,若是對父對象進行了改變,不會影響到深拷貝的副本,同理反之亦然。this
關於數組的淺拷貝,首先咱們須要嘮嗑一下Array類提供的API:concat、slice;這兩個方法都會返回一個新數組,因此不少人一開始都誤覺得這是屬於深拷貝的,其實否則。spa
MDN中關於concat的描述很是清楚:concat
方法不會改變this
或任何做爲參數提供的數組,而是返回一個淺拷貝,它包含與原始數組相結合的相同元素的副本。具體請看下面案例:code
強調:對象
下面的例中,咱們一直都會使用複雜類型來稱呼,而且在案例中,複雜類型其實咱們都是使用了JS中的對象,此時有基本知識不牢固的同窗就會納悶,爲何不直接說對象。請注意!JS中的複雜類型有兩種,一種是Object,一種是Array;實質上function也是複雜類型的一種,可是實質上Array和function都屬於Object!據此,在數組中若是元素是數組,那麼跟元素是對象,在拷貝時都是同樣的,下面只展現了是對象時的案例,是礙於篇幅的問題,請各位轉換思路便可。blog
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()方法,根據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方法,都是能夠實現數組和對象的深拷貝的。
可是仔細去品味,你會發現實質數組的深拷貝也是針對的複雜類型而言的,因此咱們實現了數組的深拷貝,就意味着咱們同時也能夠實現複雜類型的深拷貝!據此咱們同時把數組和對象的深拷貝放在一塊兒演示。
首先咱們看一下,這種方式下複製的對象之間是一種怎樣的關係:
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.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) { 6 //判斷傳遞進來的參數是對象仍是數組,並根據實際初始化一個數組或對象
7 var clone = obj instanceof Array ? [] : {}; 8 for (var key in obj) { 9 //排除掉原型鏈上的可枚舉屬性
10 if (obj.hasOwnProperty(key)) { 11 //判斷屬性是否爲空 是複雜類型仍是基本類型
12 if (obj[key] && typeof obj[key] === 'object') { 13 //若是是複雜類型,那麼遞歸調用
14 clone[key] = deepClone(obj[key]); 15 } else { 16 //屬於基本數據數據類型直接添加
17 clone[key] = obj[key]; 18 } 19 } 20 } 21 //返回深拷貝的對象
22 return clone; 23 } 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);
最終輸出的結果以下: