在網(wǎng)上瀏覽了不少關(guān)于深淺拷貝的解析,在此,要總結(jié)的幾點(diǎn)內(nèi)容大致如下:
- 基本類(lèi)型和引用類(lèi)型
- 淺拷貝與深拷貝的區(qū)別
- 常用的淺拷貝與深拷貝的方法
基本類(lèi)型和引用類(lèi)型
ECMAScript 中的變量類(lèi)型分為兩類(lèi):
基本類(lèi)型:undefined,null,布爾值(boolean),字符串(string),數(shù)值(number)
引用類(lèi)型: 統(tǒng)稱(chēng)為Object類(lèi)型,細(xì)分的話,有:Object類(lèi)型,Array類(lèi)型,Date類(lèi)型,F(xiàn)unction類(lèi)型等。
這里補(bǔ)充一下,很多人認(rèn)為null也是引用類(lèi)型,因?yàn)?typeof null === 'object',之所以會(huì)這樣可能是js語(yǔ)言設(shè)計(jì)的缺陷,與底層的二進(jìn)制判斷有關(guān)。其實(shí)null是基本類(lèi)型的變量,null不指向任何變量,它是一個(gè)常量,所以說(shuō)是基本類(lèi)型。引用類(lèi)型的變量其實(shí)也是基本類(lèi)型,而引用指向的對(duì)象本身才是引用類(lèi)型。
原理是這樣的,不同的對(duì)象在底層都表示為二進(jìn)制,在javascript中二進(jìn)制前3位都為0的話會(huì)被判斷為object類(lèi)型,null的二進(jìn)制表示全為0,自然前3位也是0,所以執(zhí)行typeof 時(shí)會(huì)返回''object'' —— 來(lái)自《你不知道的javascript》
基本類(lèi)型的變量
基本類(lèi)型的變量保存在棧內(nèi)存,棧內(nèi)存中分別存儲(chǔ)著變量的標(biāo)識(shí)符以及變量的值(盜圖一張如下)

var a = 'hello, world'
引用類(lèi)型變量
引用類(lèi)型 保存在 堆內(nèi)存 中, 棧內(nèi)存存儲(chǔ)的是變量的標(biāo)識(shí)符以及對(duì)象在堆內(nèi)存中的存儲(chǔ)地址,當(dāng)需要訪問(wèn)引用類(lèi)型(如對(duì)象,數(shù)組等)的值時(shí),首先從棧中獲得該對(duì)象的地址指針,然后再?gòu)膶?duì)應(yīng)的堆內(nèi)存中取得所需的數(shù)據(jù)。(盜圖一張如下)

var a = {name: 'jack'}

上述2種類(lèi)型是如何復(fù)制的
1 基本類(lèi)型的復(fù)制: 當(dāng)你把基本類(lèi)型的變量復(fù)制給一個(gè)新的變量的時(shí)候,相當(dāng)于把基本類(lèi)型的值賦值給了新的變量。
var a = 'jack'
var b = a
console.log(a === b)
a = 'bob'
console.log(a)
console.log(b)

從上述結(jié)果可以看出,改變a 的值不會(huì)影響 b 的值。
2 引用類(lèi)型的復(fù)制:當(dāng)把引用類(lèi)型賦值給一個(gè)新的變量的時(shí)候,實(shí)際上復(fù)制了指向堆內(nèi)存的地址,原變量和新變量指向同一個(gè)對(duì)象。
var a = {name: 'jack' , age: '30'}
var b = a
console.log(a === b)
a.age = 30
console.log(a)
console.log(b)

從上述結(jié)果可以看出,改變a 的值,b的值也發(fā)生了改變
配圖過(guò)程如下:



淺拷貝和深拷貝的區(qū)別
對(duì)于僅僅是復(fù)制了引用(地址),換句話說(shuō),復(fù)制了之后,原來(lái)的變量和新的變量指向同一個(gè)東西,彼此之間的操作會(huì)互相影響,為 淺拷貝。
而如果是在堆中重新分配內(nèi)存,擁有不同的地址,但是值是一樣的,復(fù)制后的對(duì)象與原來(lái)的對(duì)象是完全隔離,互不影響,為 深拷貝。
深淺拷貝 的主要區(qū)別就是:復(fù)制的是引用(地址)還是復(fù)制的是實(shí)例。

從上述圖可以看出是深拷貝。
下面看下淺拷貝的實(shí)現(xiàn)
淺拷貝的實(shí)現(xiàn)
var obj = {
name: 'willie',
friends: ['jack', 'rose', 'bob'],
address: {
province: 'guangdong',
city: 'shenzhen'
}
}
/**
*
* 對(duì)象的潛拷貝
* @param {ojects} obj
* @returns object
*/
function shallowCopy (obj) {
var copyObject = {}
for (var attr in obj) {
if (obj.hasOwnProperty(attr)) {
copyObject[attr] = obj[attr]
}
}
return copyObject
}
var person = shallowCopy(obj)
console.log(person === obj)
person.friends.push('tom')
console.log(person)
console.log(obj)
運(yùn)行結(jié)果如下:可以看出淺拷貝對(duì)于引用類(lèi)型,只是復(fù)制了地址,他們指向同一個(gè)對(duì)象。

深拷貝的實(shí)現(xiàn)
var obj = {
name: 'willie',
friends: ['jack', 'rose', 'bob'],
address: {
province: 'guangdong',
city: 'shenzhen'
}
}
/**
*
* 對(duì)象的深度復(fù)制:引用類(lèi)型對(duì)象不會(huì)出現(xiàn)共享
* @param {object} obj
*/
function deepCopy (obj) {
// 首先判斷參數(shù)是否存在以及參數(shù)是否是一個(gè)對(duì)象
if (!obj && typeof obj !== 'object') {
throw new Error('error params')
}
// 判斷參數(shù)是對(duì)組類(lèi)型還是對(duì)象類(lèi)型
var deepObject = Array.isArray(obj) ? [] : {}
for (var attr in obj) {
// 判斷屬性是否為元素自身的
if (obj.hasOwnProperty(attr)) {
// 判斷屬性值存在并且屬性值還是為一個(gè)引用類(lèi)型的值(數(shù)組或?qū)ο螅瑒t進(jìn)行遞歸,直到拷貝到基本屬性值為止
if(obj[attr] && typeof obj[attr] === 'object') {
deepObject[attr] = deepCopy(obj[attr]) //進(jìn)行遞歸操作
} else {
// 非引用類(lèi)型的值,直接進(jìn)行賦值
deepObject[attr] = obj[attr]
}
}
}
return deepObject
}
var person = deepCopy(obj)
console.log(person === obj)
person.friends.push('tom')
console.log(person)
console.log(obj)
運(yùn)行結(jié)果如下:當(dāng)對(duì)引用類(lèi)型進(jìn)行操作時(shí),并不會(huì)影響另一個(gè)變量

常用的淺拷貝與深拷貝的方法
- slice 和 concat
var a = [1,2,3,4]
var b = a.slice()
console.log(a === b)
b.push(5)
console.log(a)
console.log(b)

var a = [1,2,3,4]
var b = a.concat()
console.log(a === b)
b.push(5)
console.log(a)
console.log(b)

看到以上,會(huì)認(rèn)為,slice和concat是深拷貝方法,那繼續(xù)往下看
var a = [[1,2,3],4,5];
var b = a.slice();
console.log(a === b);
b[0][0] = 6;
console.log(a);
console.log(b);

從以上結(jié)果可以看到,這兩個(gè)方法都不是真正的深拷貝,concat同上。
- jQuery中的 extend 復(fù)制方法
可以用來(lái)擴(kuò)展對(duì)象,這個(gè)方法可以傳入一個(gè)參數(shù):deep(true or false),表示是否執(zhí)行深復(fù)制(如果是深復(fù)制則會(huì)執(zhí)行遞歸復(fù)制)。
// 深拷貝
var obj = {name:"jack",age:24};
//extend方法,第一個(gè)參數(shù)為true,為深拷貝,為false,或者沒(méi)有為淺拷貝。
var obj_extend = $.extend(true,{}, obj);
console.log(obj === obj_extend); // false
obj.name = "rose";
console.log(obj); // 輸出 {name:"rose",age:24}
console.log(obj_extend); 輸出 {name:"jack",age:24}
// 淺拷貝
var obj = {name:"jack",age:24};
//extend方法,第一個(gè)參數(shù)為true,為深拷貝,為false,或者沒(méi)有為淺拷貝。
var obj_extend = $.extend(false,{}, obj);
console.log(obj === obj_extend); // false
obj.name = "rose";
console.log(obj); // 輸出 {name:"rose",age:24}
console.log(obj_extend); // 輸出 {name:"rose",age:24}
效果怎么會(huì)一樣呢
其實(shí)總結(jié)一下就是:
Array 的 slice 和 concat 方法 和 jQuery 中的 extend 復(fù)制方法,他們都會(huì)復(fù)制第一層的值,對(duì)于 第一層 的值都是 深拷貝,而到 第二層 的時(shí)候 Array 的 slice 和 concat 方法就是 復(fù)制引用 ,jQuery 中的 extend 復(fù)制方法 則 取決于 你的 第一個(gè)參數(shù), 也就是是否進(jìn)行遞歸復(fù)制。所謂第一層 就是 key 所對(duì)應(yīng)的 value 值是基本數(shù)據(jù)類(lèi)型,也就像上面例子中的name、age,而對(duì)于 value 值是引用類(lèi)型 則為第二層,例如{name: 'jack', address: {province: 'guangdong', city: 'shenzhen'}}
- JSON 對(duì)象的 parse 和 stringify
JOSN 對(duì)象中的 stringify 可以把一個(gè) js 對(duì)象序列化為一個(gè) JSON 字符串,parse 可以把 JSON 字符串反序列化為一個(gè) js 對(duì)象,這兩個(gè)方法實(shí)現(xiàn)的是深拷貝。
var obj = {name:'jack', age:24, address : {province : 'guangdong',city : 'shenzhen'} };
var obj_json = JSON.parse(JSON.stringify(obj));
console.log(obj === obj_json);
obj.address.city = "fushan";
obj.name = "abc";
console.log(obj);
console.log(obj_json);

從以上可以看出,進(jìn)行了深拷貝