彻底理解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


写在最后: 约定优于配置-------软件开发的简约原则.
-------------------------------- (完)--------------------------------------

我的:
个人网站: https://neveryu.github.io/neveryu/
Github: https://github.com/Neveryu
新浪微博: https://weibo.com/Neveryu

更多学习资源请关注我的新浪微博…

猜你喜欢

转载自blog.csdn.net/csdn_yudong/article/details/80326848