js深浅拷贝

Posted zzh0318

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了js深浅拷贝相关的知识,希望对你有一定的参考价值。

在js中,经常要对数组进行拷贝操作,但如果只是简单的将它赋予其他变量,那么之后只需要修改一个变量,其他的就都会受到影响一起改变。这便是数组的深浅拷贝问题,像这种直接赋值的方式就是浅拷贝,但很多时候,这样并不是我们想要得到的结果。

举个例子:

var arr1 = [0,1,2,3];
var arr2 = arr1;
arr2[1] = "hello";
console.log(arr1);   // [0,‘hello‘,2,3]

由上可知,修改了 arr2 之后,arr1 也随之改变了。同样,在js中对象的拷贝操作也是如此。

var obj1 = {
   name: ‘test‘,
   color: ‘blue‘
}
var obj2 = obj1;
obj2.color = ‘red‘;
console.log(obj1.color);   // ‘red‘

 

1. 深浅拷贝

js大致分成两种数据类型:基本数据类型和引用数据类型。

基本数据类型保存在栈内存,而引用类型则保存在堆内存中。对于基本数据类型的拷贝,并没有深浅拷贝的区别,我们所说的深浅拷贝都是对于引用数据类型而言的。

浅拷贝:浅拷贝是复制引用,复制后的引用都是指向同一个对象的实例,彼此之间的操作会互相影响。

深拷贝:深拷贝不是简单的复制引用,而是在堆中重新分配内存,并且把源对象实例的所有属性都进行新建复制,以保证深拷贝的对象的引用图不包含任何原有对象或对象图上的任何对象,复制后的对象与原来的对象是完全隔离的。

 

2. 浅拷贝的实现

像上面两个例子这样的直接复制引用方式便是浅拷贝。

 

3. 深拷贝的实现

a、Array的slice和concat方法

var arr1 = [0,1,2,3];
var arr2 = arr1;
var arr3 = arr1.slice();
var arr4 = arr1.concat();
arr3[1] = "hello";
arr4[1] = "world";
console.log(arr1);      // [0,1,2,3]
console.log(arr3);      // [0,‘hello‘,2,3]
console.log(arr4);      // [0,‘world‘,2,3]
console.log(arr1 == arr2);   // true
console.log(arr1 == arr3);   // false
console.log(arr1 == arr4);   // false

由上面的代码可知,Array的slice和concat方法都会返回一个新的数组实例,并且自己的操作不会影响到其他对象。但如果数组是多层的话,情况就会不同了。

var arr1 = [0,[1,1,1,1],2,3,{name: ‘blue‘}];
var arr2 = arr1;
var arr3 = arr1.slice();
var arr4 = arr1.concat();
arr3[1][1] = "0";
arr4[4].name = "red";
console.log(arr1)      // [0,[1,0,1,1],2,3,{name: ‘red‘}]
console.log(arr2)      // [0,[1,0,1,1],2,3,{name: ‘red‘}]
console.log(arr3)      // [0,[1,0,1,1],2,3,{name: ‘red‘}]
console.log(arr4)      // [0,[1,0,1,1],2,3,{name: ‘red‘}]

如果像上面的例子,数组是多层的,那么slice和concat方法只能对第一层进行深拷贝。

 

b、JSON对象的parse和stringify

JSON对象的parse方法可以将JSON字符串反序列化成JS对象,stringify方法可以将JS对象序列化成JSON字符串。

var obj1 = {
  name: ‘father‘,
  child: {
    name: ‘son‘
  },
  number: [1,1,1,1]
};
var obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj1 == obj2);     // false
obj2.name = "father_change";
obj2.child.name = "son_change";
obj2.number[1] = 0;
console.log(obj1);        // {name: ‘father‘, child: {name: ‘son‘}, number: [1,1,1,1]}
console.log(obj2);        // {name: ‘father_change‘, child: {name: ‘son_change‘}, number: [1,0,1,1]}

由上例子可知,拷贝之后的obj2和obj1也是相互隔离的,借助这两个方法,也可以实现对象的深拷贝。但是如果对象中包含一个函数,就会出现一些问题。

var obj3 = {
  name: ‘father‘,
  say: function(){
    console.log(‘Hello World‘);
  }
};
var obj4 = JSON.parse(JSON.stringify(obj3));
console.log(obj3);        // {name: ‘father‘, say: f()}
console.log(obj4);        // {name: ‘father‘}

如上情况,obj3中包含一个函数say,拷贝后的obj4中并没有找到say函数。所以说如果对象中含有一个函数时,就不能用这个方法进行深拷贝。

 

c、递归拷贝

递归拷贝就是对每一层的数据都实现一次 创建对象->对象赋值 的操作,代码如下:

function deepClone(obj){
  let objClone = obj.constructor === Array ? [] : {}; // 判断复制的目标是数组还是对象
  for(let i in obj){
    if(obj.hasOwnProperty(i)){
      // 如果值是对象,就递归一下,不是就直接复制
      if(obj[i] && typeof obj[i] === ‘object‘){
        objClone[i] = deepClone(obj[i]);
      } else {
        objClone[i] = obj[i];
      }
    }
  }
  return objClone;
}

var obj1 = {
  name: ‘father‘,
  child: {
    name: ‘son‘
  },
  number: [1,1,1,1],
  say: function(){
    console.log(‘Hello World‘);
  }
};
var obj2 = deepClone(obj1);
console.log(obj1 == obj2);     // false
obj2.name = "father_change";
obj2.child.name = "son_change";
obj2.number[1] = 0;
console.log(obj1);        // {name: ‘father‘, child: {name: ‘son‘}, number: [1,1,1,1], say: f()}
console.log(obj2);        // {name: ‘father_change‘, child: {name: ‘son_change‘}, number: [1,0,1,1], say: f()}

可以看到,obj2在通过递归拷贝之后,也可以实现与obj1的相互隔离,所以说递归拷贝也是一种深拷贝方式。并且,使用递归方法进行拷贝不会忽视对象中的函数,所以使用递归方法可以实现完全意义上的深拷贝。

 

综上:

slice和concat方法只能进行第一层的深拷贝(如果只想对一些简单基本的对象进行深拷贝,用这两个方法即可);

JSON.parse和JSON.stringify能实现深拷贝,但会忽视对象中的函数(如果想对多层的但不含有函数的对象进行深拷贝,可用此方法);

递归方法则可以实现真正意义上的深拷贝。

以上是关于js深浅拷贝的主要内容,如果未能解决你的问题,请参考以下文章

学姐面试宝典前端基础篇Ⅴ——JS深浅拷贝箭头函数事件监听等

简述深浅拷贝

js深浅拷贝

js的深浅拷贝

最简js深浅拷贝说明

JS深浅拷贝