Fork me on GitHub

探究JS中对象的深拷贝和浅拷贝

深拷贝和浅拷贝的区别

在讲深拷贝和浅拷贝的区别之前,回想一下我们平时拷贝一个对象时是怎么操作的?是不是像这样?

1
2
3
4
var testObj1 = {a: 1, b:2}, testObj2=testObj1;
testObj1.a = 7;
console.log(testObj1); //{a: 7, b:2}
console.log(testObj2); //{a: 7, b:2}

发现问题了吗?当testObj1变化时,testObj2相应的属性跟着变化了。这就是属于浅拷贝了,而所谓的深拷贝就是要做到当testObj1变化时testObj2不会跟着变化,这就是浅拷贝与深拷贝的区别。至少在我知道基本类型和引用类型的区别之前我是不知道为什么会这样的,那什么是基本类型和引用类型呢?

基本类型和引用类型

首先通过一个简单的例子看看与上面的例子有什么区别:

1
2
3
4
var num1 = 1, num2 = num1;
num1 = 7;
console.log(num1); // 7
console.log(num2); // 1

很显然,这里的变量num2并没有因为num1的变化而变化。其实,这里的num1和num2就是一种基本类型,而上面的那两个对象变量则属于引用类型。

ECMAScript变量可能包含两种不同数据类型的值:基本类型值和引用类型值。基本类型值指的是那些保存在栈内存中的简单数据段,即这种值完全保存在内存中的一个位置。而引用类型值是指那些保存堆内存中的对象,意思是变量中保存的实际上只是一个指针,这个指针指向内存中的另一个位置,该位置保存对象。

所以,按我的理解,基本类型就是直接存在内存里的一个具体点,相互之间是独立的,而引用类型存储的只是一个指向具体内存地址的指针,当两个对象相等赋值时,他们实际上是指向的同一个内存地址,所以,当一个变了,另一个跟着变也就不奇怪了。打个不恰当的比喻,基本类型之间就像独立的小超市,相互之间的门头都是不一样的,而连锁超市的话都是一样的,而且都会一起变化。

如何实现深拷贝

既然已经知道了深拷贝和浅拷贝的区别以及为什么会出现这种区别,那怎么实现深拷贝呢?我之前在业务开发中有遇到过这个问题,但都是采用先新建一个空对象,再遍历目标对象的属性,将目标对象的属性值一个个赋值给这个新的空对象,从而得到了一个新的对象,这个对象是完全等于之前的对象的,但是不会受它变化而影响,所以算是初步实现了深拷贝的,大体实现如下:

1
2
3
4
5
6
7
8
var testObj3 = {a: 1, b:2, c:3, d:4};
var testObj4 = {};
for(var key in testObj3){
testObj4[key] = testObj3[key];
}
testObj3.a = 7;
console.log(testObj3); //{a: 7, b:2, c:3, d:4}
console.log(testObj4); //{a: 1, b:2, c:3, d:4}

咋一看这个方法当时虽然满足了我当时的业务需求,可是还有什么可以改进的地方呢?还有别的实现方式吗?

还有什么别的实现方式吗?

之前我也没细想过这个问题,知道后来偶尔间看到这篇文章,这篇文章提供的其他方式是我之前不知道,算是补上了我之前知识的盲区,在这里便是感谢。总结来说,还有以下几种方式:

  • 1.通过Onject.assign()

    • Object.assign是ES6中引入的一种用于合并对象的方法,具体使用方法可以看文档
    • 这个方法我之前是用的不多的,但最近几个项目中有些使用后算是知道了这个属性,其实当时就应该可以想到可以用于深拷贝的,大体用法如下:

      1
      2
      3
      4
      var obj1 = {x: 1, y:2}, obj2 = Object.assign({}, obj1);
      obj2.x = 7;
      console.log(obj1); //{a: 7, b:2}
      console.log(obj2); //{a: 1, b:2}
    • 可以看到,完美地深拷贝了这个对象,新对象并没有受原对象影响,可是,当对象为嵌套对象的时候呢?

      1
      2
      3
      4
      var obj3 = {x:1, y:2, z:{name: 3}}, obj4 = Object.assign({}, obj3);
      obj4.z.name = 7;
      console.log(obj3); //{x:1, y:2, z:{name: 7}}
      console.log(obj4); //{x:1, y:2, z:{name: 7}}
    • 事实证明,对于嵌套对象,Object.assign()深拷贝失效了,所以说,Object.assign()只能实现一维对象的深拷贝

  • 2.通过JSON.parse(JSON.stringify(obj))

    • 这个方法也是在前一段时间朋友问我深拷贝的方法时偶尔查到的,之前是没用的,感觉还蛮好用的:

      1
      2
      3
      4
      var obj5 = {x: 1, y:2}, obj6 = JSON.parse(JSON.stringify(obj5));
      obj6.x = 7;
      console.log(obj5); //{x: 1, y:2}
      console.log(obj6); //{x: 7, y:2}
    • 可是这个方法能解决多维对象的深拷贝问题吗?

      1
      2
      3
      4
      var obj9 = {x:1, y:2, z:{name: 3}}, obj10 = Object.assign({}, obj3);
      obj10.z.name = 7;
      console.log(obj9); //{x:1, y:2, z:{name: 3}}
      console.log(obj10); //{x:1, y:2, z:{name: 7}}
    • 从代码上来看,这个方法完美地实现了多维对象的深拷贝,可是这样小小改动一下就会发现有问题了:

      1
      2
      3
      4
      var obj9 = {x:1, y:2, z:{name: 3, func: function(){}}}, obj10 = Object.assign({}, obj3);
      obj10.z.name = 7;
      console.log(obj9); //{x:1, y:2, z:{name: 3, func: function(){}}}}
      console.log(obj10); //{x:1, y:2, z:{name: 7}}
    • 是的,当多维对象里有函数时,并没有复制,这是为什么,这个方法的MDN上有解释:

      undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。

    • 所以,JSON.parse(JSON.stringify(obj))这种方法虽然可以拷贝多维对象,但不能深拷贝含有undefined、function、symbol值的对象。
可以改进的地方
  • 最开始那种通过for.in遍历所有属性的方式是会将原型链上的属性也遍历出来的,一般来说是不要的,所以最好使用obj.hasOwnProperty(key)来进行筛选判断,当然具体情况要根据业务需求来。
  • 后面的几种方法都没有能完美地实现多维数组的深拷贝,所以还得祭出大杀器:递归。简单来说,就是先新建一个新对象,然后通过Object.keys()遍历原对象的所有属性列表,再遍历这个列表,如果有子集也是对象再递归一次,最终得到了深拷贝的对象:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    function deepCopy(obj) {
    let result = {};
    let keys = Object.keys(obj), key=null, tem=null;
    for(var 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
    }
    console.log(deepCopy({x:1, y:3}))
    var obj7 = {x:1, y:3, z:{name: 7, func: function(){}}};
    var obj8 = deepCopy(obj7);
    obj8.z.name = 8;
    console.log(obj8); // {x:1, y:3, z:{name: 8, func: function(){}}}
    console.log(obj7) // {x:1, y:3, z:{name: 7, func: function(){}}}

这样,就得到了一个可以兼容多维对象,并且可以实现对function,null,symbol值等特殊值的深拷贝。同时,我们也可以利用第三方库实现深拷贝,如jquery的$.extend和lodash的_.cloneDeep。

针对一种特殊情况的处理

本来上面的代码已经几乎完美地实现了深拷贝,但是,有一种特殊情况,就是当拷贝的对象本来就是一个循环引用的对象时,再去递归,那就无穷无尽会爆栈了。像这样的:

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

我参考的处理方案就是加一层股票率判断,虽然我觉得这种情况根本就没有什么现实存在的意义,但就当一种临界值的处理吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function deepCopy(obj, parent) {
let result = {};
let keys = Object.keys(obj), key=null, tem=null, parent_=parent;
if (parent_) {
if (parent_.originalParent === obj) {
return parent_.curentParent;
}
parent_ = parent_.parent;
}
for(var i=0; i<keys.length; i++) {
key = keys[i];
temp = obj[key];
if (temp && typeof temp === 'object') {
result[key] = deepCopy(temp, {
originalParent: obj,
curentParent: result,
parent: parent
});
}
else{
result[key] = temp;
}
}
return result
}
obj1.z = obj1;
var obj2 = deepCopy(obj1);
console.log(obj2); //很多东西

这样,一个深拷贝函数就完成了,对于数组的话是同样适用的,毕竟数组也是特殊的对象嘛,当然实际项目中使用第三方库可能还会更加方便一点,很多时候造轮子并不是为了在项目中使用而是为了了解轮子的构造,这个很重要,之后还会继续造轮子的,这是个深入过膝的过程,加油!

参考文章: