「JavaScript」帶你完全搞清楚深拷貝、淺拷貝和循環引用

1、爲何有深拷貝和淺拷貝?

     這個要從js中的數據類型提及,js中數據類型分爲基本數據類型引用數據類型jquery

    基本類型值指的是那些保存在內存中的簡單數據段,即這種值是徹底保存在內存中的一個位置。包含NumberStringBooleanNullUndefinedSymbol正則表達式

    引用類型值指的是那些保存在內存中的對象,因此引用類型的值保存的是一個指針,這個指針指向存儲在中的一個對象。除了上面的 6 種基本數據類型外,剩下的就是引用類型了,統稱爲 Object 類型。細分的話,有:Object 類型、Array 類型、Date 類型、RegExp 類型、Function 類型 等。數組

    正由於引用類型的這種機制, 當咱們從一個變量向另外一個變量複製引用類型的值時,其實是將這個引用類型在內存中的引用地址複製了一份給新的變量,其實就是一個指針。所以當操做結束後,這兩個變量實際上指向的是同一個在內存中的對象,改變其中任意一個對象,另外一個對象也會跟着改變。函數

圖片描述

    所以深拷貝和淺拷貝只發生在引用類型中。簡單來講他們的區別在於:spa

1. 層次

  • 淺拷貝 只會將對象的各個屬性進行依次複製,並不會進行遞歸複製,也就是說只會賦值目標對象的第一層屬性。
  • 深拷貝不一樣於淺拷貝,它不僅拷貝目標對象的第一層屬性,而是遞歸拷貝目標對象的全部屬性。

2. 是否開闢新的棧

  • 淺拷貝 對於目標對象第一層爲基本數據類型的數據,就是直接賦值,即「傳值」;而對於目標對象第一層爲引用數據類型的數據,就是直接賦存於棧內存中的堆內存地址,即「傳址」,並沒有開闢新的棧,也就是複製的結果是兩個對象指向同一個地址,修改其中一個對象的屬性,則另外一個對象的屬性也會改變,
  • 深拷貝 而深複製則是開闢新的棧,兩個對象對應兩個不一樣的地址,修改一個對象的屬性,不會改變另外一個對象的屬性。

2、淺拷貝

如下是實現淺拷貝的幾種實現方式:指針

1.Array.concat()

const arr = [1,2,3,4,[5,6]];
   const copy = arr.concat(); \\ 利用concat()建立arr的副本
   
   \\改變基本類型值,不會改變原數組
   copy[0] = 2; 
   arr; //[1,2,3,4,[5,6]];

   \\改變數組中的引用類型值,原數組也會跟着改變
   copy[4][1] = 7;
   arr; //[1,2,3,4,[5,7]];

能實現相似效果的還有slice()和Array.from()等,你們能夠本身嘗試一下~code

2.Object.assign()

const obj1 = {x: 1, y: 2};
const obj2 = Object.assign({}, obj1);

obj2.x = 2; \\修改obj2.x,改變對象中的基本類型值
console.log(obj1) //{x: 1, y: 2} //原對象未改變
console.log(obj2) //{x: 2, y: 2}
const obj1 = {
    x: 1, 
    y: {
        m: 1
    }
};
const obj2 = Object.assign({}, obj1);

obj2.y.m = 2; \\修改obj2.y.m,改變對象中的引用類型值
console.log(obj1) //{x: 1, y: {m: 2}} 原對象也被改變
console.log(obj2) //{x: 2, y: {m: 2}}

3、深拷貝

1.JSON.parse()和JSON.stringify()

const obj1 = {
    x: 1, 
    y: {
        m: 1
    }
};
const 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格式能表示的全部數據類型,可是有如下幾個缺點:對象

  • undefined、任意的函數、正則表達式類型以及 symbol 值,在序列化過程當中會被忽略(出如今非數組對象的屬性值中時)或者被轉換成 null(出如今數組中時);
  • 它會拋棄對象的constructor。也就是深拷貝以後,無論這個對象原來的構造函數是什麼,在深拷貝以後都會變成Object;
  • 若是對象中存在循環引用的狀況沒法正確處理。

2.遞歸

function deepCopy1(obj) {
    // 建立一個新對象
    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;
}

const obj1 = {
    x: {
        m: 1
    },
    y: undefined,
    z: function add(z1, z2) {
        return z1 + z2
    },
    a: Symbol("foo")
};

const obj2 = deepCopy1(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)}

4、循環引用

看似遞歸已經徹底解決咱們的問題了,然而還有一種狀況咱們沒考慮到,那就是循環引用blog

1.父級引用

這裏的父級引用指的是,當對象的某個屬性,正是這個對象自己,此時咱們若是進行深拷貝,可能會在子元素->父對象->子元素...這個循環中一直進行,致使棧溢出。好比下面這個例子:遞歸

const obj1 = {
    x: 1, 
    y: 2
};
obj1.z = obj1;

const obj2 = deepCopy1(obj1); \\棧溢出

解決辦法是:只須要判斷一個對象的字段是否引用了這個對象或這個對象的任意父級便可,能夠修改上面的deepCopy1函數:

function deepCopy2(obj, parent=null) {
    //建立一個新對象
    let result = {};
    let keys = Object.keys(obj),
         key = null,
         temp = null,
         _parent = parent;
    //該字段有父級則須要追溯該字段的父級
    while(_parent) {
        //若是該字段引用了它的父級,則爲循環引用
        if(_parent.originParent === obj) {
            //循環引用返回同級的新對象
            return _parent.currentParent;
        }
        _parent = _parent.parent
    }
    for(let i=0,len=keys.length;i<len;i++) {
        key = keys[i]
        temp = obj[key]
        // 若是字段的值也是一個新對象
        if(temp && typeof temp === 'object') {
            result[key] = deepCopy(temp, {
                //遞歸執行深拷貝,將同級的待拷貝對象與新對象傳遞給parent,方便追溯循環引用
                originParent: obj,
                currentParent: result,
                parent: parent
            });
        } else {
            result[key] = temp;
        }
    }
    return result;
}

const obj1 = {
    x:1
}
obj1.z = obj1;

const obj2 = deepCopy2(obj1);

2. 同級引用

假設對象obj有a,b,c三個子對象,其中子對象c中有個屬性d引用了對象obj下面的子對象a。

const obj= {
    a: {
        name: 'a'
    },
    b: {
        name: 'b'
    },
    c: {

    }
};
c.d.e = obj.a;

此時c.d.e和obj.a 是相等的,由於它們引用的是同一個對象

console.log(c.d.e === obj.a); //true

若是咱們調用上面的deepCopy2函數

const copy = deepCopy2(obj);
console.log(copy.a); // 輸出: {name: "a"}
console.log(copy.d.e);// 輸出: {name: "a"}
console.log(copy.a === copy.d.e); // 輸出: false

以上表現咱們就能夠看出,雖然opy.a 和copy.d.e在字面意義上是相等的,但兩者並非引用的同一個對象,這點上來看對象copy和原對象obj仍是有差別的。

這種狀況是由於obj.a並不在obj.d.e的父級對象鏈上,因此deepCopy2函數就沒法檢測到obj.d.e對obj.a也是一種引用關係,因此deepCopy2函數就將obj.a深拷貝的結果賦值給了copy.d.e。

解決方案:父級的引用是一種引用,非父級的引用也是一種引用,那麼只要記錄下對象A中的全部對象,並與新建立的對象一一對應便可。

function deepCopy3(obj) {
    // hash表,記錄全部的對象的引用關係
    let map = new WeakMap();
    function dp(obj) {
        let result = null;
        let keys = Object.keys(obj);
        let key = null,
            temp = null,
            existobj = null;

        existobj = map.get(obj);
        //若是這個對象已經被記錄則直接返回
        if(existobj) {
            return existobj;
        }

        result = {}
        map.set(obj, result);

        for(let i =0,len=keys.length;i<len;i++) {
            key = keys[i];
            temp = obj[key];
            if(temp && typeof temp === 'object') {
                result[key] = dp(temp);
            }else {
                result[key] = temp;
            }
        }
        return result;
    }
    return dp(obj);
}

const obj= {
    a: {
        name: 'a'
    },
    b: {
        name: 'b'
    },
    c: {

    }
};
c.d.e = obj.a;

const copy = deepCopy3(obj);

5、總結

    其實拷貝的方式還有不少種,好比jquery中的$.extend,lodash的_.cloneDeep等等,關於拷貝中還有不少問題值得深究,好比正則類型的值如何拷貝,原型上的屬性如何拷貝,這些我都會慢慢研究噠!你們也能夠思考一下~
    最後,歡迎點贊收藏!!錯誤之處歡迎指正(`・ω・´)

相關文章
相關標籤/搜索