underscore 的源碼中,有很多地方用到了 Array.prototype.slice() 方法,但是并沒有傳參,實際上只是為了返回數組的副本,例如 underscore 中 clone 的方法:
// Create a (shallow-cloned) duplicate of an object.// 對象的 `淺復制` 副本// 注意點:所有嵌套的對象或者數組都會跟原對象用同一個引用// 所以是為淺復制,而不是深度克隆_.clone=function(obj){// 容錯,如果不是對象或者數組類型,則可以直接返回// 因為一些基礎類型是直接按值傳遞的if(!_.isObject(obj))returnobj;// 如果是數組,則用 obj.slice() 返回數組副本// 如果是對象,則提取所有 obj 的鍵值對覆蓋空對象,返回return_.isArray(obj)?obj.slice():_.extend({},obj);};
這里就涉及到了一個知識點:深淺拷貝。
所謂深淺拷貝,都是進行復制,那么區別主要在于復制出來的新對象和原來的對象是否會互相影響,改一個,另一個也會變。
淺拷貝栗子:
vara=["a","b","c"];vara_slice=a;console.log(a===a_slice);a_slice[0]="f";console.log(a_slice);console.log(a);
深拷貝栗子:
varobj=[[1,2,3],4,5];varobj_extend=$.extend(true,{},obj);//extend方法,第一個參數為true,為深拷貝,為false,或者沒有為淺拷貝。console.log(obj===obj_extend);obj[0][0]="heihei";console.log(obj);console.log(obj_extend);
有了上面的大概認識,讓我們從原理深入了解一下深淺拷貝。
一、基本類型 和 引用類型
1、ECMAScript 中的變量類型分為兩類:
基本類型:undefined,null,布爾值(Boolean),字符串(String),數值(Number)
引用類型: 統稱為Object類型,細分的話,有:Object類型,Array類型,Date類型,Function類型等。
2、不同類型的存儲方式:
基本數據類型保存在棧內存,形式如下:棧內存中分別存儲著變量的標識符以及變量的值。
即
vara="A";
在棧內存中是這樣的
引用類型保存在堆內存中,棧內存存儲的是變量的標識符以及對象在堆內存中的存儲地址,當需要訪問引用類型(如對象,數組等)的值時,首先從棧中獲得該對象的地址指針,然后再從對應的堆內存中取得所需的數據。
即
var a = {name:“jack”};
在內存中是這樣的
3、不同類型的復制方式:
基本類型的復制:當你在復制基本類型的時候,相當于把值也一并復制給了新的變量。
栗子 1:
vara=1;varb=a;console.log(a===b);vara=2;console.log(a);console.log(b);
改變 a 變量的值,并不會影響 b 的值。
內存中是這樣的:
vara=1;
var b = a;
a = 2;
引用類型的復制:當你在復制引用類型的時候,實際上只是復制了指向堆內存的地址,即原來的變量與復制的新變量指向了同一個東西。
栗子 2:
vara={name:"jack",age:20};varb=a;console.log(a===b);a.age=30;console.log(a);console.log(b);
改變 a 變量的值,會影響 b 的值。
內存中是這樣的:
var a = {name:"jack",age:20};
var b = a;
a.age = 30;
二、明白了上面之后,所謂深淺拷貝:
對于僅僅是復制了引用(地址),換句話說,復制了之后,原來的變量和新的變量指向同一個東西,彼此之間的操作會互相影響,為淺拷貝。
而如果是在堆中重新分配內存,擁有不同的地址,但是值是一樣的,復制后的對象與原來的對象是完全隔離,互不影響,為深拷貝。
深淺拷貝的主要區別就是:復制的是引用(地址)還是復制的是實例。
所以上面的栗子2,如何可以變成深拷貝呢?
我們可以想象出讓 b 在內存中像下圖這樣,肯定就是深拷貝了。
那么代碼上如何實現呢?
利用遞歸來實現深復制,對屬性中所有引用類型的值,遍歷到是基本類型的值為止。
functiondeepClone(source){if(!source&&typeofsource!=='object'){thrownewError('error arguments','shallowClone');}vartargetObj=Array.isArray(source)?[]:{};for(varkeysinsource){if(source.hasOwnProperty(keys)){if(source[keys]&&typeofsource[keys]==='object'){targetObj[keys]=deepClone(source[keys]);//遞歸}else{targetObj[keys]=source[keys];}}}returntargetObj;}
檢測一下
vara={name:"jack",age:20};varb=deepClone(a);console.log(a===b);a.age=30;console.log(a);console.log(b);
三 、最后讓我們來看看 一些 js 中的 復制方法,他們到底是深拷貝還是淺拷貝?
1、 Array 的 slice 和 concat 方法
兩者都會返回一個新的數組實例。
栗子:
slice:
vara=[1,2,3];varb=a.slice();//sliceconsole.log(b===a);a[0]=4;console.log(a);console.log(b);
concat:
vara=[1,2,3];varb=a.concat();//concatconsole.log(b===a);a[0]=4;console.log(a);console.log(b);
看到結果,如果你覺得,這兩個方法是深復制,那就恭喜你跳進了坑里
讓咱們再看一個顛覆你觀念的栗子:
vara=[[1,2,3],4,5];varb=a.slice();console.log(a===b);a[0][0]=6;console.log(a);console.log(b);
看見了嗎?都變啦!
這就是坑,知道嗎?
所以你要記住的是 Array的 slice 和 concat 方法 并不是真正的深拷貝,他們其實是披著羊(qian)皮(kao)的(bei)狼。
2、 jQuery中的 extend 復制方法
可以用來擴展對象,這個方法可以傳入一個參數:deep(true or false),表示是否執行深復制(如果是深復制則會執行遞歸復制)。
栗子:
深拷貝:
varobj={name:'xixi',age:20,company:{name:'騰訊',address:'深圳'}};varobj_extend=$.extend(true,{},obj);//extend方法,第一個參數為true,為深拷貝,為false,或者沒有為淺拷貝。console.log(obj===obj_extend);obj.company.name="ali";obj.name="hei";console.log(obj);console.log(obj_extend);
淺拷貝:
varobj={name:"xixi",age:20};varobj_extend=$.extend(false,{},obj);//extend方法,第一個參數為true,為深拷貝,為false,或者沒有為淺拷貝。console.log(obj===obj_extend);obj.name="heihei";console.log(obj);console.log(obj_extend);
咦,company 的變化 可以看出 深淺復制來(即箭頭所指), 紅色方框圈出的地方,怎么和上面 slice 和 concat 的情況一樣?難道也是羊?
其實總結一下就是:
Array 的 slice 和 concat 方法 和 jQuery 中的 extend 復制方法,他們都會復制第一層的值,對于第一層的值都是深拷貝,而到第二層的時候 Array 的 slice 和 concat 方法就是復制引用,jQuery 中的 extend 復制方法 則取決于 你的 第一個參數, 也就是是否進行遞歸復制。所謂第一層 就是 key 所對應的 value 值是基本數據類型,也就像上面栗子中的name、age,而對于 value 值是引用類型 則為第二層,也就像上面栗子中的 company。
3、JSON 對象的 parse 和 stringify
JOSN 對象中的 stringify 可以把一個 js 對象序列化為一個 JSON 字符串,parse 可以把 JSON 字符串反序列化為一個 js 對象,這兩個方法實現的是深拷貝。
栗子:
var obj = {name:'xixi',age:20,company : { name : '騰訊', address : '深圳'} };var obj_json = JSON.parse(JSON.stringify(obj));console.log(obj === obj_json);obj.company.name = "ali";obj.name = "hei";console.log(obj);console.log(obj_json);
完全的 深拷貝。