JavaScript 的深拷貝和淺拷貝
在 JavaScript 的變量指派操作中,如果一個變量值是簡單類型,直接複制沒有問題。但如果是對象或者數組,直接複制後的新對象或者數組隻要一修改,原對象或者數組就會同樣跟着被修改。如果你不了解深拷貝和淺拷貝,你可能就會覺得這是 bug,不可了解。但是看完本文你就能了解了。
什麼是深拷貝和淺拷貝?
淺拷貝比較容易了解,是以先從淺拷貝開始說起吧!
淺拷貝
就是将一個對象(或數組)的記憶體位址『編号』複制給另一個對象(或數組)
深拷貝
增加一個指針,并且申請一個新的記憶體位址,使這個增加的指針指向這個新的記憶體,然後将原變量對應記憶體位址裡的值逐個複制過去
深拷貝和淺拷貝怎麼實作?
數組
eg1. 淺拷貝
var arr = ["One", "Two", "Three"];
console.log("原數組的值:" + arr);
var newArr = arr;
newArr[] = "newTwo";
console.log("新數組的值:" + newArr);
console.log("淺拷貝後,原數組的值:" + arr);
運作結果:
因為 arr 淺拷貝指派給 newArr 的是一個指向 arr 記憶體位址的引用,是以修改 newArr 的第2個元素的值,會将原數組 arr 的第二個元素值也修改掉了(其實在記憶體裡,兩個數組共用一個記憶體空間)。
eg2. 深拷貝
var arr = ["One", "Two", "Three"];
console.log("原數組的值:" + arr);
var newArr = arr.slice();
newArr[] = "newTwo";
console.log("新數組的值:" + newArr);
console.log("深拷貝後,原數組的值:" + arr);
運作結果:
因為深拷貝是給 newArr 申請一個新的記憶體位址,并使 newArr 的指針指向這個新的記憶體。然後再把 arr 對應記憶體的值逐個複制到這個新記憶體位址。是以這時再修改 newArr 的第2個元素的值,就不會将原數組 arr 的第二個元素值也修改掉了(在記憶體裡,兩個數組不再共用一個記憶體空間了)。
對象
eg1. 淺拷貝
var obj = {
name: 'xu',
age: ,
sex: 'men',
}
console.log("原對象的值:");
console.log(obj);
var newObj = obj;
newObj.age = ;
console.log("新對象的值:");
console.log(newObj);
console.log("淺拷貝後,原對象的值:");
console.log(obj);
運作結果:
對象的淺拷貝跟數組一樣,因為指派過去的是原對象的引用,是以修改了新對象 newObj 的屬性 age 後,原對象屬性 age 也被修改了。
eg2. 深拷貝
var obj = {
name: 'xu',
age: ,
sex: 'men',
}
console.log("原對象的值:");
console.log(obj);
var newObj = {};
for (var i in obj) {
newObj[i] = obj[i];
}
newObj.age = ;
console.log("新對象的值:");
console.log(newObj);
console.log("深拷貝後,原對象的值:");
console.log(obj);
運作結果:
這是針對一個對象中的所有屬性值都是基本資料類型的情況。但是如果一個對象的屬性值還是對象這種情況,就需要遞歸調用淺拷貝。可以如下處理:
eg. 對象深拷貝拓展
function deepCopy(obj, copyObj) {
var newObj = copyObj || {};
for (var i in obj) {
if (typeof obj[i] === 'object') {
newObj[i] = (obj[i].constructor === Array) ? [] : {};
deepCopy(obj[i], newObj[i]);
} else {
newObj[i] = obj[i];
}
}
return newObj;
}
var obj = {
name: {
oldName: 'pan',
newName: 'zhengmao'
},
age: ,
sex: 'men',
}
console.log("原對象的 name 屬性值:");
console.log(obj.name);
var secObj = {};
secObj = deepCopy(obj, secObj); // 将 obj 的所有屬性都複制到 secObj 中
secObj.name = {
oldName: 'pan2',
newName: 'zhengmao2'
}
console.log("新對象的的 name 屬性值:");
console.log(secObj.name);
console.log("深拷貝後,原對象的 name 屬性值:");
console.log(obj.name);
運作結果:
實作原理,其實就是先建立一個空對象,在記憶體中新開辟一塊位址,把被複制對象的所有可枚舉的(注意可枚舉的對象)屬性方法一一複制過來,注意要用遞歸來複制子對象裡面的所有屬性和方法,直到所有子代屬性都為基本資料類型。
對象深拷貝的方法還有不少:
- 對象拷貝方法:
b = Object.assign({}, a)
- 字元串對象互轉方法結合:
b = JSON.parse( JSON.stringify(a) )
結論
對于簡單的資料類型(如 Number、String、Boolean 等),JavaScript 變量指派即是值複制(深拷貝)。但是對象或者數組,直接指派其實複制的隻是記憶體位址(淺拷貝)。這樣錯誤的操作會讓父對象或父數組存随時在被篡改的可能。這時,如需實作深拷貝則需要跟文中示例一樣,自己另行處理。