理解JavaScript中的深拷貝和淺拷貝

在說深拷貝與淺拷貝前,我們先看兩個簡單的案例:

//案例1
var num1 = 1, num2 = num1;
console.log(num1) //1
console.log(num2) //1
num2 = 2; //修改num2
console.log(num1) //1
console.log(num2) //2
//案例2
var obj1 = {x: 1, y: 2}, obj2 = obj1;
console.log(obj1) //{x: 1, y: 2}
console.log(obj2) //{x: 1, y: 2}
obj2.x = 2; //修改obj2.x
console.log(obj1) //{x: 2, y: 2}
console.log(obj2) //{x: 2, y: 2}

按照常規(guī)思維,obj1應(yīng)該和num1一樣,不會因為另外一個值的改變而改變,而這里的obj1 卻隨著obj2的改變而改變了。同樣是變量,為什么表現(xiàn)不一樣呢?這就要引入JS中基本類型和引用類型的概念了。

基本類型和引用類型

ECMAScript變量可能包含兩種不同數(shù)據(jù)類型的值:基本類型值和引用類型值?;绢愋椭抵傅氖悄切┍4嬖跅?nèi)存中的簡單數(shù)據(jù)段,即這種值完全保存在內(nèi)存中的一個位置。而引用類型值是指那些保存堆內(nèi)存中的對象,意思是變量中保存的實際上只是一個指針,這個指針指向內(nèi)存中的另一個位置,該位置保存對象。

打個比方,基本類型和引用類型在賦值上的區(qū)別可以按“連鎖店”和“單店”來理解:基本類型賦值等于在一個新的地方安裝連鎖店的規(guī)范標(biāo)準(zhǔn)新開一個分店,新開的店與其他舊店互不相關(guān),各自運營;而引用類型賦值相當(dāng)于一個店有兩把鑰匙,交給兩個老板同時管理,兩個老板的行為都有可能對一間店的運營造成影響。

上面清晰明了的介紹了基本類型和引用類型的定義和區(qū)別。目前基本類型有:
Boolean、Null、Undefined、Number、String、Symbol,引用類型有:Object、Array、Function。之所以說“目前”,因為Symbol就是ES6才出來的,之后也可能會有新的類型出來。

再回到前面的案例,案例1中的值為基本類型,案例2中的值為引用類型。案例2中的賦值就是典型的淺拷貝,并且深拷貝與淺拷貝的概念只存在于引用類型。

深拷貝與淺拷貝

既然已經(jīng)知道了深拷貝與淺拷貝的來由,那么該如何實現(xiàn)深拷貝?我們先分別看看Array和Object自有方法是否支持:

Array

var arr1 = [1, 2], arr2 = arr1.slice();
console.log(arr1); //[1, 2]
console.log(arr2); //[1, 2]
arr2[0] = 3; //修改arr2
console.log(arr1); //[1, 2]
console.log(arr2); //[3, 2]

此時,arr2的修改并沒有影響到arr1,看來深拷貝的實現(xiàn)并沒有那么難嘛。我們把arr1改成二維數(shù)組再來看看:

var arr1 = [1, 2, [3, 4]], arr2 = arr1.slice();
console.log(arr1); //[1, 2, [3, 4]]
console.log(arr2); //[1, 2, [3, 4]]
arr2[2][1] = 5;
console.log(arr1); //[1, 2, [3, 5]]
console.log(arr2); //[1, 2, [3, 5]]

咦,arr2又改變了arr1,看來slice()只能實現(xiàn)一維數(shù)組的深拷貝。

具備同等特性的還有:concat、Array.from() 。

Object

1、Object.assign()

var obj1 = {x: 1, y: 2}, obj2 = Object.assign({}, obj1);
console.log(obj1) //{x: 1, y: 2}
console.log(obj2) //{x: 1, y: 2}
obj2.x = 2; //修改obj2.x
console.log(obj1) //{x: 1, y: 2}
console.log(obj2) //{x: 2, y: 2}
var obj1 = {
x: 1,
y: {
m: 1
}
};
var obj2 = Object.assign({}, obj1);
console.log(obj1) //{x: 1, y: {m: 1}}
console.log(obj2) //{x: 1, y: {m: 1}}
obj2.y.m = 2; //修改obj2.y.m
console.log(obj1) //{x: 1, y: {m: 2}}
console.log(obj2) //{x: 2, y: {m: 2}}

經(jīng)測試,Object.assign()也只能實現(xiàn)一維對象的深拷貝。

2、JSON.parse(JSON.stringify(obj))

var obj1 = {
x: 1,
y: {
m: 1
}
};
var obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj1) //{x: 1, y: {m: 1}}
console.log(obj2) //{x: 1, y: {m: 1}}
obj2.y.m = 2; //修改obj2.y.m
console.log(obj1) //{x: 1, y: {m: 1}}
console.log(obj2) //{x: 2, y: {m: 2}}

JSON.parse(JSON.stringify(obj)) 看起來很不錯,不過MDN文檔 的描述有句話寫的很清楚:

undefined、任意的函數(shù)以及 symbol 值,在序列化過程中會被忽略(出現(xiàn)在非數(shù)組對象的屬性值中時)或者被轉(zhuǎn)換成 null(出現(xiàn)在數(shù)組中時)。

我們再來把obj1改造下:

var obj1 = {
x: 1,
y: undefined,
z: function add(z1, z2) {
return z1 + z2
},
a: Symbol("foo")
};
var obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj1) //{x: 1, y: undefined, z: ?, a: Symbol(foo)}
console.log(JSON.stringify(obj1)); //{"x":1}
console.log(obj2) //{x: 1}

發(fā)現(xiàn),在將obj1進(jìn)行JSON.stringify()序列化的過程中,y、z、a都被忽略了,也就驗證了MDN文檔的描述。既然這樣,那JSON.parse(JSON.stringify(obj))的使用也是有局限性的,不能深拷貝含有undefined、function、symbol值的對象,不過JSON.parse(JSON.stringify(obj))簡單粗暴,已經(jīng)滿足90%的使用場景了。

經(jīng)過驗證,我們發(fā)現(xiàn)JS 提供的自有方法并不能徹底解決Array、Object的深拷貝問題。只能祭出大殺器:遞歸

function deepCopy(obj) {
// 創(chuàng)建一個新對象
let result = {}
let keys = Object.keys(obj),
key = null,
temp = null;
for (let i = 0; i < keys.length; i++) {
key = keys[i];
temp = obj[key];
// 如果字段的值也是一個對象則遞歸操作
if (temp && typeof temp === 'object') {
result[key] = deepCopy(temp);
} else {
// 否則直接賦值給新對象
result[key] = temp;
}
}
return result;
}
var obj1 = {
x: {
m: 1
},
y: undefined,
z: function add(z1, z2) {
return z1 + z2
},
a: Symbol("foo")
};
var obj2 = deepCopy(obj1);
obj2.x.m = 2;
console.log(obj1); //{x: {m: 1}, y: undefined, z: ?, a: Symbol(foo)}
console.log(obj2); //{x: {m: 2}, y: undefined, z: ?, a: Symbol(foo)}

可以看到,遞歸完美的解決了前面遺留的所有問題,我們也可以用第三方庫:jquery的$.extend和lodash的_.cloneDeep來解決深拷貝。上面雖然是用Object驗證,但對于Array也同樣適用,因為Array也是特殊的Object。

到這里,深拷貝問題基本可以告一段落了。但是,還有一個非常特殊的場景:

循環(huán)引用拷貝

var obj1 = {
x: 1,
y: 2
};
obj1.z = obj1;
var obj2 = deepCopy(obj1);

此時如果調(diào)用剛才的deepCopy函數(shù)的話,會陷入一個循環(huán)的遞歸過程,從而導(dǎo)致爆棧。jquery的$.extend也沒有解決。解決這個問題也非常簡單,只需要判斷一個對象的字段是否引用了這個對象或這個對象的任意父級即可,修改一下代碼:

function deepCopy(obj, parent = null) {
// 創(chuàng)建一個新對象
let result = {};
let keys = Object.keys(obj),
key = null,
temp= null,
_parent = parent;
// 該字段有父級則需要追溯該字段的父級
while (_parent) {
// 如果該字段引用了它的父級則為循環(huán)引用
if (_parent.originalParent === obj) {
// 循環(huán)引用直接返回同級的新對象
return _parent.currentParent;
}
_parent = _parent.parent;
}
for (let i = 0; i < keys.length; i++) {
key = keys[i];
temp= obj[key];
// 如果字段的值也是一個對象
if (temp && typeof temp=== 'object') {
// 遞歸執(zhí)行深拷貝 將同級的待拷貝對象與新對象傳遞給 parent 方便追溯循環(huán)引用
result[key] = DeepCopy(temp, {
originalParent: obj,
currentParent: result,
parent: parent
});
} else {
result[key] = temp;
}
}
return result;
}
var obj1 = {
x: 1,
y: 2
};
obj1.z = obj1;
var obj2 = deepCopy(obj1);
console.log(obj1); //太長了去瀏覽器試一下吧~
console.log(obj2); //太長了去瀏覽器試一下吧~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 在說深拷貝與淺拷貝前,我們先看兩個簡單的案例: 按照常規(guī)思維,obj1應(yīng)該和num1一樣,不會因為另外一個值的改變...
    Simbawu閱讀 548評論 0 4
  • 1、新的聲明方式 以前我們在聲明時只有一種方法,就是使用var來進(jìn)行聲明,ES6對聲明的進(jìn)行了擴(kuò)展,現(xiàn)在可以有三種...
    令武閱讀 1,091評論 0 7
  • 第2章 基本語法 2.1 概述 基本句法和變量 語句 JavaScript程序的執(zhí)行單位為行(line),也就是一...
    悟名先生閱讀 4,569評論 0 13
  • 在 JS 中有一些基本類型像是Number、String、Boolean,而對象就是像這樣的東西{ name: '...
    tobAlier閱讀 649評論 0 0
  • 使用Charles進(jìn)行HTTPS抓包1.配置2.Charles抓取https時一直顯示unknown
    xxxxxxxxx_ios閱讀 239評論 0 0

友情鏈接更多精彩內(nèi)容