浅谈深拷贝和浅拷贝

深拷贝和浅拷贝

提及深拷贝和浅拷贝,起首我们来看两个栗子

// 栗子1
var a = 1,b=a;
console.log(a);
console.log(b)
b = 2;
console.log(a);
console.log(b)
// 栗子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}

根据惯性头脑,栗子1中obj1应当跟a一样,不会因别的一个值的转变而转变的啊,而这里倒是obj1随着obj2的转变而转变了?一样都是变量,怎样就表现不一样了呢?岂非存在品级上的好坏?此处须要寻思一小会。要处理这个题目,就要引入一个JS中基础范例和援用范例的观点了。

基础范例和援用范例

ECMAScript变量包含两种差别数据范例的值:基础范例值和援用范例值。基础范例值指的是那些保留在栈内存中的简朴数据段,即这类值完整保留在内存中的一个位置。而援用范例值是指那些保留堆内存中的对象,意义是变量中保留的实际上只是一个指针,这个指针指向内存中的另一个位置,该位置保留对象。

两类数据的保留体式格局

《浅谈深拷贝和浅拷贝》

从上图能够看到,栈内存重要用于存储种种基础范例的变量,包含Boolean、Number、String、Undefined、Null等以及对象变量的指针。而堆内存重要担任对象Object这类变量范例的存储。现在基础范例有:
Boolean、Null、Undefined、Number、String、Symbol,援用范例有:Object、Array、Function。Symbol就是ES6才出来的,以后也能够会有新的范例出来。

让我们再回到前面的案例,栗子1中的值为基础范例,栗子2中的值为援用范例,栗子2中的赋值就是典范的浅拷贝。我们须要明白一点,深拷贝与浅拷贝的观点只存在于援用范例。

既然已知道了深拷贝与浅拷贝的因由,那末该怎样完成深拷贝?我们离别来看看Array和Object自有要领是不是支撑:

var arr1 = [1, 2];
var 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]];
var 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()只能完成一维数组的深拷贝,并不能完成真正的深拷贝。与之有一致特征的另有:concat、Array.from() 。

研讨完Array,我们来看看Object

var obj1 = {x: 1, y: 2};
var 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()跟Array一样也只能完成一维对象的深拷贝。形成只能完成一维对象深拷贝的原因是第一层的属性确切完成了深拷贝,具有了自力的内存,但更深的属性却依然公用了地点,所以才会形成上面的题目。

那怎样真正的完成援用范例的深拷贝呢?接下来要有请正主入场

1.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(出现在数组中时)。概况能够戳这里
MDN文档

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}

经实践证明,在将obj1举行JSON.stringify()序列化的历程当中,y、z、a都被疏忽了,也就考证了MDN文档的形貌。既然如许,那JSON.parse(JSON.stringify(obj))的运用也是有局限性的,不能深拷贝含有undefined、function、symbol值的对象,不过JSON.parse(JSON.stringify(obj))简朴粗犷,已满足90%的运用场景了。
经由考证,我们发明JS 供应的自有要领并不能彻底处理Array、Object的深拷贝题目。只能祭出大杀器:递归

2.递归

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)}

能够看到,递归圆满的处理了前面遗留的一切题目。然则,另有一个异常特别极度的场景:轮回援用拷贝

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

var obj2 = deepCopy(obj1);

此时假如挪用适才的deepCopy函数的话,会堕入一个轮回的递归历程,从而致使爆栈。处理这个题目也异常简朴,只须要推断一个对象的字段是不是援用了这个对象或这个对象的恣意父级即可

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); 

总结

  • 简朴的一维条理的拷贝能够应用数组本身要领和对象的Object.assign完成,在二维条理上要领失效,没法完成深拷贝
  • 简朴粗犷的罕见的拷贝能够经由过程JSON.parse(JSON.stringify(obj))完成,但关于属性的某些特别范例的值失效。
  • 最终要领,用递归完成援用范例的深拷贝
  • 固然另有其他要领,比方运用第三方库内封装的要领
    原文作者:suan_suan
    原文地址: https://segmentfault.com/a/1190000018351707
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞