本文思維導(dǎo)圖如下:
本文首發(fā)于我的個(gè)人網(wǎng)站: http://cherryblog.site/
本文作者: Cherry
本文鏈接: http://cherryblog.site/deepcopy.html
版權(quán)聲明: 本博客所有文章除特別聲明外,均采用 CC BY-NC-SA 3.0 許可協(xié)議。轉(zhuǎn)載請(qǐng)注明出處??!
前言
最近在讀 zepto 的源碼,深有感觸,感覺(jué)隨便一段代碼都可以延伸出一大堆的知識(shí)點(diǎn),在看到深拷貝和淺拷貝的時(shí)候,之前只是了解過(guò)什么是深拷貝什么是淺拷貝,并沒(méi)有對(duì)齊實(shí)現(xiàn)進(jìn)行探索,所以本文主要講一下什么是深拷貝、什么是淺拷貝、深拷貝與淺拷貝的區(qū)別,以及怎么進(jìn)行深拷貝和怎么進(jìn)行淺拷貝。
堆和棧的區(qū)別
其實(shí)深拷貝和淺拷貝的主要區(qū)別就是其在內(nèi)存中的存儲(chǔ)類型不同。
堆和棧都是內(nèi)存中劃分出來(lái)用來(lái)存儲(chǔ)的區(qū)域。
棧(stack)為自動(dòng)分配的內(nèi)存空間,它由系統(tǒng)自動(dòng)釋放;而堆(heap)則是動(dòng)態(tài)分配的內(nèi)存,大小不定也不會(huì)自動(dòng)釋放。
ECMAScript 的數(shù)據(jù)類型
在將深拷貝和淺拷貝之前,我們先來(lái)重新回顧一下 ECMAScript 中的數(shù)據(jù)類型。主要分為
基本數(shù)據(jù)類型(undefined,boolean,number,string,null)
基本數(shù)據(jù)類型主要是:undefined,boolean,number,string,null。
基本數(shù)據(jù)類型存放在棧中
存放在棧內(nèi)存中的簡(jiǎn)單數(shù)據(jù)段,數(shù)據(jù)大小確定,內(nèi)存空間大小可以分配,是直接按值存放的,所以可以直接訪問(wèn)。
基本數(shù)據(jù)類型值不可變
javascript中的原始值(undefined、null、布爾值、數(shù)字和字符串)與對(duì)象(包括數(shù)組和函數(shù))有著根本區(qū)別。原始值是不可更改的:任何方法都無(wú)法更改(或“突變”)一個(gè)原始值。對(duì)數(shù)字和布爾值來(lái)說(shuō)顯然如此 —— 改變數(shù)字的值本身就說(shuō)不通,而對(duì)字符串來(lái)說(shuō)就不那么明顯了,因?yàn)樽址雌饋?lái)像由字符組成的數(shù)組,我們期望可以通過(guò)指定索引來(lái)假改字符串中的字符。實(shí)際上,javascript 是禁止這樣做的。字符串中所有的方法看上去返回了一個(gè)修改后的字符串,實(shí)際上返回的是一個(gè)新的字符串值。
基本數(shù)據(jù)類型的值是不可變的,動(dòng)態(tài)修改了基本數(shù)據(jù)類型的值,它的原始值也是不會(huì)改變的,例如:
var str = "abc";
console.log(str[1]="f"); // f
console.log(str); // abc
這一點(diǎn)其實(shí)開(kāi)始我是比較迷惑的,總是感覺(jué) js 是一個(gè)靈活的語(yǔ)言,任何值應(yīng)該都是可變的,真是圖樣圖森破,我們通常情況下都是對(duì)一個(gè)變量重新賦值,而不是改變基本數(shù)據(jù)類型的值。就如上述引用所說(shuō)的那樣,在 js 中沒(méi)有方法是可以改變布爾值和數(shù)字的。倒是有很多操作字符串的方法,但是這些方法都是返回一個(gè)新的字符串,并沒(méi)有改變其原有的數(shù)據(jù)。
所以,記住這一點(diǎn):基本數(shù)據(jù)類型值不可變。
基本類型的比較是值的比較
基本類型的比較是值的比較,只要它們的值相等就認(rèn)為他們是相等的,例如:
var a = 1;
var b = 1;
console.log(a === b);//true
比較的時(shí)候最好使用嚴(yán)格等,因?yàn)?== 是會(huì)進(jìn)行類型轉(zhuǎn)換的,比如:
var a = 1;
var b = true;
console.log(a == b);//true
引用類型
基本數(shù)據(jù)類型存放在堆中
引用類型(object)是存放在堆內(nèi)存中的,變量實(shí)際上是一個(gè)存放在棧內(nèi)存的指針,這個(gè)指針指向堆內(nèi)存中的地址。每個(gè)空間大小不一樣,要根據(jù)情況開(kāi)進(jìn)行特定的分配,例如。
ar person1 = {name:'jozo'};
var person2 = {name:'xiaom'};
var person3 = {name:'xiaoq'};
引用類型值可變
引用類型是可以直接改變其值的,例如:
var a = [1,2,3];
a[1] = 5;
console.log(a[1]); // 5
引用類型的比較是引用的比較
所以每次我們對(duì) js 中的引用類型進(jìn)行操作的時(shí)候,都是操作其對(duì)象的引用(保存在棧內(nèi)存中的指針),所以比較兩個(gè)引用類型,是看其的引用是否指向同一個(gè)對(duì)象。例如:
var a = [1,2,3];
var b = [1,2,3];
console.log(a === b); // false
雖然變量 a 和變量 b 都是表示一個(gè)內(nèi)容為 1,2,3 的數(shù)組,但是其在內(nèi)存中的位置不一樣,也就是說(shuō)變量 a 和變量 b 指向的不是同一個(gè)對(duì)象,所以他們是不相等的。
(懶癌晚期,不想自己畫(huà)圖了,直接盜圖)
傳值與傳址
了解了基本數(shù)據(jù)類型與引用類型的區(qū)別之后,我們就應(yīng)該能明白傳值與傳址的區(qū)別了。
在我們進(jìn)行賦值操作的時(shí)候,基本數(shù)據(jù)類型的賦值(=)是在內(nèi)存中新開(kāi)辟一段棧內(nèi)存,然后再把再將值賦值到新的棧中。例如:
var a = 10;
var b = a;
a ++ ;
console.log(a); // 11
console.log(b); // 10
所以說(shuō),基本類型的賦值的兩個(gè)變量是兩個(gè)獨(dú)立相互不影響的變量。
但是引用類型的賦值是傳址。只是改變指針的指向,例如,也就是說(shuō)引用類型的賦值是對(duì)象保存在棧中的地址的賦值,這樣的話兩個(gè)變量就指向同一個(gè)對(duì)象,因此兩者之間操作互相有影響。例如:
var a = {}; // a保存了一個(gè)空對(duì)象的實(shí)例
var b = a; // a和b都指向了這個(gè)空對(duì)象
a.name = 'jozo';
console.log(a.name); // 'jozo'
console.log(b.name); // 'jozo'
b.age = 22;
console.log(b.age);// 22
console.log(a.age);// 22
console.log(a == b);// true
淺拷貝
在深入了解之前,我認(rèn)為上面的賦值就是淺拷貝,哇哈哈,真的是圖樣圖森破。上面那個(gè)應(yīng)該只能算是“引用”,并不算是真正的淺拷貝。
一下部分參照知乎中的提問(wèn): javascript中的深拷貝和淺拷貝
賦值(=)和淺拷貝的區(qū)別
那么賦值和淺拷貝有什么區(qū)別呢,我們看下面這個(gè)例子:
var obj1 = {
'name' : 'zhangsan',
'age' : '18',
'language' : [1,[2,3],[4,5]],
};
var obj2 = obj1;
var obj3 = shallowCopy(obj1);
function shallowCopy(src) {
var dst = {};
for (var prop in src) {
if (src.hasOwnProperty(prop)) {
dst[prop] = src[prop];
}
}
return dst;
}
obj2.name = "lisi";
obj3.age = "20";
obj2.language[1] = ["二","三"];
obj3.language[2] = ["四","五"];
console.log(obj1);
//obj1 = {
// 'name' : 'lisi',
// 'age' : '18',
// 'language' : [1,[4,5]],
//};
console.log(obj2);
//obj2 = {
// 'name' : 'lisi',
// 'age' : '18',
// 'language' : [1,[4,5]],
//};
console.log(obj3);
//obj3 = {
// 'name' : 'zhangsan',
// 'age' : '20',
// 'language' : [1,[4,5]],
//};
先定義個(gè)一個(gè)原始的對(duì)象 obj1,然后使用賦值得到第二個(gè)對(duì)象 obj2,然后通過(guò)淺拷貝,將 obj1 里面的屬性都賦值到 obj3 中。也就是說(shuō):
-
obj1:原始數(shù)據(jù) -
obj2:賦值操作得到 -
obj3:淺拷貝得到
然后我們改變 obj2 的 name 屬性和 obj3 的 name 屬性,可以看到,改變賦值得到的對(duì)象 obj2 同時(shí)也會(huì)改變?cè)贾?obj1,而改變淺拷貝得到的的 obj3 則不會(huì)改變?cè)紝?duì)象 obj1。這就可以說(shuō)明賦值得到的對(duì)象 obj2 只是將指針改變,其引用的仍然是同一個(gè)對(duì)象,而淺拷貝得到的的 obj3 則是重新創(chuàng)建了新對(duì)象。
然而,我們接下來(lái)來(lái)看一下改變引用類型會(huì)是什么情況呢,我又改變了賦值得到的對(duì)象 obj2 和淺拷貝得到的 obj3 中的 language 屬性的第二個(gè)值和第三個(gè)值(language 是一個(gè)數(shù)組,也就是引用類型)。結(jié)果見(jiàn)輸出,可以看出來(lái),無(wú)論是修改賦值得到的對(duì)象 obj2 和淺拷貝得到的 obj3 都會(huì)改變?cè)紨?shù)據(jù)。
這是因?yàn)闇\拷貝只復(fù)制一層對(duì)象的屬性,并不包括對(duì)象里面的為引用類型的數(shù)據(jù)。所以就會(huì)出現(xiàn)改變淺拷貝得到的 obj3 中的引用類型時(shí),會(huì)使原始數(shù)據(jù)得到改變。
深拷貝:將 B 對(duì)象拷貝到 A 對(duì)象中,包括 B 里面的子對(duì)象,
淺拷貝:將 B 對(duì)象拷貝到 A 對(duì)象中,但不包括 B 里面的子對(duì)象
| -- | 和原數(shù)據(jù)是否指向同一對(duì)象 | 第一層數(shù)據(jù)為基本數(shù)據(jù)類型 | 原數(shù)據(jù)中包含子對(duì)象 |
|---|---|---|---|
| 賦值 | 是 | 改變會(huì)使原數(shù)據(jù)一同改變 | 改變會(huì)使原數(shù)據(jù)一同改變 |
| 淺拷貝 | 否 | 改變不會(huì)使原數(shù)據(jù)一同改變 | 改變會(huì)使原數(shù)據(jù)一同改變 |
| 深拷貝 | 否 | 改變不會(huì)使原數(shù)據(jù)一同改變 | 改變不會(huì)使原數(shù)據(jù)一同改變 |
深拷貝
看了這么半天,你也應(yīng)該清楚什么是深拷貝了吧,如果還不清楚,我就剖腹自盡(?_?)
深拷貝是對(duì)對(duì)象以及對(duì)象的所有子對(duì)象進(jìn)行拷貝。
那么問(wèn)題來(lái)了,怎么進(jìn)行深拷貝呢?
思路就是遞歸調(diào)用剛剛的淺拷貝,把所有屬于對(duì)象的屬性類型都遍歷賦給另一個(gè)對(duì)象即可。我們直接來(lái)看一下 Zepto 中深拷貝的代碼:
// 內(nèi)部方法:用戶合并一個(gè)或多個(gè)對(duì)象到第一個(gè)對(duì)象
// 參數(shù):
// target 目標(biāo)對(duì)象 對(duì)象都合并到target里
// source 合并對(duì)象
// deep 是否執(zhí)行深度合并
function extend(target, source, deep) {
for (key in source)
if (deep && (isPlainObject(source[key]) || isArray(source[key]))) {
// source[key] 是對(duì)象,而 target[key] 不是對(duì)象, 則 target[key] = {} 初始化一下,否則遞歸會(huì)出錯(cuò)的
if (isPlainObject(source[key]) && !isPlainObject(target[key]))
target[key] = {}
// source[key] 是數(shù)組,而 target[key] 不是數(shù)組,則 target[key] = [] 初始化一下,否則遞歸會(huì)出錯(cuò)的
if (isArray(source[key]) && !isArray(target[key]))
target[key] = []
// 執(zhí)行遞歸
extend(target[key], source[key], deep)
}
// 不滿足以上條件,說(shuō)明 source[key] 是一般的值類型,直接賦值給 target 就是了
else if (source[key] !== undefined) target[key] = source[key]
}
// Copy all but undefined properties from one or more
// objects to the `target` object.
$.extend = function(target){
var deep, args = slice.call(arguments, 1);
//第一個(gè)參數(shù)為boolean值時(shí),表示是否深度合并
if (typeof target == 'boolean') {
deep = target;
//target取第二個(gè)參數(shù)
target = args.shift()
}
// 遍歷后面的參數(shù),都合并到target上
args.forEach(function(arg){ extend(target, arg, deep) })
return target
}
在 Zepto 中的 $.extend 方法判斷的第一個(gè)參數(shù)傳入的是一個(gè)布爾值,判斷是否進(jìn)行深拷貝。
在 $.extend 方法內(nèi)部,只有一個(gè)形參 target,這個(gè)設(shè)計(jì)你真的很巧妙。
因?yàn)樾螀⒅挥幸粋€(gè),所以 target 就是傳入的第一個(gè)參數(shù)的值,并在函數(shù)內(nèi)部設(shè)置一個(gè)變量 args 來(lái)接收去除第一個(gè)參數(shù)的其余參數(shù),如果該值是一個(gè)布爾類型的值的話,說(shuō)明要啟用深拷貝,就將 deep 設(shè)置為 true,并將 target 賦值為 args 的第一個(gè)值(也就是真正的 target)。如果該值不是一個(gè)布爾類型的話,那么傳入的第一個(gè)值仍為 target 不需要進(jìn)行處理,只需要遍歷使用 extend 方法就可以。
這里有點(diǎn)繞,但是真的設(shè)計(jì)的很精妙,建議自己打斷點(diǎn)試一下,會(huì)有意外收獲(玩轉(zhuǎn) js 的大神請(qǐng)忽略)。
而在 extend 的內(nèi)部,是拷貝的過(guò)程。
參考文章: