理解JS中的淺拷貝與深拷貝

YXi發表於2019-10-12

資料型別

在進入正題之前,先說個資料型別

在JS中,資料型別分兩種:基本資料型別、引用資料型別

基本資料型別

JS中的基本資料型別有五種: nullundefinedbooleanstringnumber
資料變數是直接按值存放的,他們的值在記憶體中佔據著固定大小的空間,並被儲存在棧記憶體中,可以直接訪問,並且是簡單的資料段,其資料型別的值是不可變的。

var str = "xxx";
str[0] = "y";
console.log(str);    // xxx
複製程式碼

基本型別的比較是值的比較
只要它們的值相等就認為他們是相等的,例如:

var a = 1;
var b = 1;
console.log(a === b);	// true
複製程式碼

引用資料型別

引用資料型別包括物件和陣列,其儲存在堆當中,而變數實際上是一個存放在棧記憶體的指標,這個指標指向堆記憶體中的地址。 當我們訪問的時候,實際上是訪問指標,然後指標去尋找物件或陣列,其資料型別的值是可變的。

var str = [1,2,3];
str[0] = 4;
console.log(str);   // [4,2,3]
複製程式碼

引用型別的比較是引用的比較
我們對 js 中的引用型別進行操作的時候,都是操作其物件的引用(儲存在棧記憶體中的指標),所以比較兩個引用型別,是看其的引用是否指向同一個物件。例如:

var a = [1,2,3];
var b = [1,2,3];
console.log(a === b);  // false
複製程式碼

圖片載入失敗!

淺拷貝與深拷貝的理解

淺拷貝

只複製指向某個物件的指標,而不復制物件本身,新舊物件共享一塊記憶體;

簡單的說,淺拷貝就是將一個物件的記憶體地址的“”編號“”複製給另一個物件。即在真正訪問的時候還是會訪問到被複制物件。 或者只是深拷貝了第一層的引用型別,而沒有拷貝更深層次的應用型別,而是利用複製地址的方式,這也是淺拷貝。

深拷貝

複製並建立一個一模一樣的物件,不共享記憶體,修改新物件,舊物件保持不變。

簡單的說,深拷貝就是先新建一個空物件,記憶體中新開闢一塊地址,把被複制物件的所有可列舉的(注意可列舉的物件)屬性方法一一複製過來,注意要用遞迴來複制子物件裡面的所有屬性和方法,直到子子.....屬性為基本資料型別。
關鍵點:開闢新記憶體、遞迴複製。

淺拷貝與深拷貝的實現方式

淺拷貝

var str1 = {
    name:"Fan"
}
var str2 = str1
str2.name = "Jun"
console.log(str1.name); // Jun
複製程式碼

這裡首先建立了一個 str1 物件,然後將str1複製給了 str2, 但是這裡僅僅是指標的複製,所以在修改 str2.name 的時候,實際上是修改的同一個堆中的物件,既淺拷貝。


var str = {
    a:1,
    b:{
        d:"Fan"
    },
    c:[1,2,3]
}
function Test(obj){
    var newStr = {};
    for (var item in obj){
        newStr[item] = obj[item];
    }
    return newStr;
}
var newStr = Test(str);
console.log(newStr.b.d === str.b.d);    // true
複製程式碼

這段程式碼是通過for in的形式將物件進行復制,這裡可以看到複製只是對於指標的複製,得到的新的物件還是指向同一個堆中的物件,所以是淺拷貝。


var str1 = {
    name: 'Fan', 
    age: 22,
    other: {
        school: 'HuangHuai'
    }
}
var str2 = Object.assign({}, str1);
str2.name = 'Jun'
console.log(str1.name)  // Fan

str2.other.school = 'ShiYan'
console.log(str1.other.school)  //ShiYan
複製程式碼

只從表面上來看,似乎Object.assign()的目標物件是{ },是一個新的物件(開闢了一塊新的記憶體空間),是深拷貝。

當我們修改str2.name的時候,str1.name沒有改變,但是當我們修改 str2.other.school 的時候,str1.other.school 同樣也發生了變化。

Object.assign()也是淺拷貝,或者說只是深拷貝了第一層,這樣我們認為它還是淺拷貝。


var a = [1, [2, 3, 4], {
    name: 'Fan'
}];
var b = a.concat(5)

a[0] = 6;
console.log(b[0]) // 1 看起來像深拷貝

a[1][0] = 999;
console.log(b[1][0]) // 999 淺拷貝

a[2].name = 'Fan'
console.log(b[2].name) // Fan 淺拷貝
複製程式碼

可以看到通過concat返回的新陣列,只有改變其中一個的布林值、字串、數值,另一個不會改變,但是改變其中的物件、陣列時,可以發現,另一個也在同時改變,即還是引用原來的堆中的內容。


var a = [1, [2, 3, 4], {
    name: 'Fan'
}];
var b = a.slice(0)

a[0] = 6;
console.log(b[0]) // 1 看起來像深拷貝

a[1][0] = 999;
console.log(b[1][0]) // 999 淺拷貝

a[2].name = 'Fan'
console.log(b[2].name) // Fan 淺拷貝

複製程式碼

這段程式碼僅僅是將上一段中的concat修改為了slice,發現結果也是一樣的,即slice方法得到的也是淺拷貝。

深拷貝

JSON.stringify() 和 JSON.parse()

var str1 = {
    name: 'Fan', 
    age: 22,
    other: {
        school: 'HuangHuai'
    }
}
var str2 = JSON.parse(JSON.stringify(str1));
str2.name = 'Jun'
console.log(str1.name) // Fan

str2.other.school = 'ShiYan'
console.log(str1.other.school) // HuangHuai
複製程式碼

可以看出通過JSON.stringify先將物件轉化為字串,然後再通過JSON.parse()轉化為物件,這個物件就是完全在開闢的新的記憶體空間中的物件 。

雖然這種方式可以實現深拷貝,但是會存在不足,就是他只能拷貝符合JSON格式的資料,如果不是JSON格式的資料,則不行.
例如:

let obj = {name:"Fan",age:function(){}}
let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj)  // { name: 'Fan' }
複製程式碼

使用遞迴實現深拷貝

function deepClone(source) {
    const targetObj = source.constructor === Array ? [] : {}; // 判斷複製的目標是陣列還是物件
    for (let keys in source) { // 遍歷目標
        if (source.hasOwnProperty(keys)) {
            if (source[keys] && typeof source[keys] === 'object') { // 如果值是物件,就遞迴一下
                targetObj[keys] = source[keys].constructor === Array ? [] : {};
                targetObj[keys] = deepClone(source[keys]);
            } else { // 如果不是,就直接賦值
                targetObj[keys] = source[keys];
            }
        }
    }
    return targetObj;
}

var str1 = {
    arr: [1, 2, 3],
    obj: {
        key: 'value'
    },
    fn: function () {
        return 1;
    }
};
var str3 = deepClone(str1);

console.log(str3 === str1); // false
console.log(str3.obj === str1.obj); // false
console.log(str3.fn === str1.fn); // true
複製程式碼

6_6

相關文章