1. 程式人生 > >徹底理解JavaScript中的深拷貝和淺拷貝

徹底理解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}

可以看到基本型別的 num1 和引用型別的 obj1 是有區別的。

基本型別和引用型別

ECMAScript 變數可能包含兩種不同資料型別的值:基本型別值和引用型別值。基本型別值指的是那些儲存在棧記憶體中的簡單資料段,即這種值完全儲存在記憶體中的一個位置。而引用型別值是指那些儲存堆記憶體中的物件,意思是變數中儲存的實際只是這個物件的引用,這個引用指向堆記憶體中的物件。

目前基本型別有:Boolean、Null、Undefined、Number、String、Symbol。
引用型別有:Object、Array、Function。

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

深拷貝與淺拷貝

既然已經知道了深拷貝與淺拷貝的來由,那麼該如何實現深拷貝?我們先分別看看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,我們把arr1改成二維陣列再來看看:

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()只能實現一維陣列的深拷貝。
具備同等特性的還有:concatArray.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}}

經測試,Object.assign()也只能實現一維物件的深拷貝

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、任意的函式以及 symbol 值,在序列化過程中會被忽略(出現在非陣列物件的屬性值中時)或者被轉換成 null(出現在陣列中時)。

我們再來把 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: f, a: Symbol(foo)}
console.log(JSON.stringify(obj1));  //{"x": 1}
console.log(obj2)  //{x: 1}

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

經過驗證,我們發現 JS 提供的自有方法並不能徹底解決 Array、Object 的深拷貝問題。只能祭出大殺器:遞迴

function deepCopy(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;
}
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$.extendlodash_.cloneDeep 來解決深拷貝。上面雖然使用 Object 驗證,但對於 Array 也同樣使用,因為 Array 也是特殊的 Object。

但是,還有一個非常特殊的場景:

迴圈引用拷貝

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

此時如果呼叫剛才的 deepCopy 函式的話,會陷入一個迴圈的遞迴過程,從而導致爆棧。 jquery$.extend 也沒有解決。解決這個問題也非常簡單,只需要判斷一個物件的欄位是否引用了這個物件或這個物件的任意父級即可,修改一下原始碼:

function deepCopy(obj, parent = null) {
    // 建立一個新物件
    let result = {};
    let keys = Object.keys(obj),
        key = null,
        temp= null,
        _parent = parent;
    // 該欄位有父級則需要追溯該欄位的父級
    while (_parent) {
        // 如果該欄位引用了它的父級則為迴圈引用
        if (_parent.originalParent === obj) {
            // 迴圈引用直接返回同級的新物件
            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') {
            // 遞迴執行深拷貝 將同級的待拷貝物件與新物件傳遞給 parent 方便追溯迴圈引用
            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); //太長了去瀏覽器試一下吧~ 

至此,已完成一個支援迴圈引用的深拷貝函式,當然,也可以使用 lodash_.cloneDeep

---------------------------(正文完)------------------------------------
一個前端的學習交流群,想進來面基的,可以點選這個logoVue學習交流,或者手動search群號:685486827

寫在最後:約定優於配置-------軟體開發的簡約原則.
--------------------------------(完)--------------------------------------

更多學習資源請關注我的新浪微博…