JS基础概念-数组的深拷贝和浅拷贝

copy也是分程度的

Posted by Niro on January 23, 2018

拷贝分为两种情况:拷贝引用和拷贝实例。

数组的浅拷贝(拷贝引用)

数组的浅拷贝就是拷贝原对象的引用,因此只要其中一个数组改变,另一个数组也会跟着改变。拷贝的数组只遍历了最外层来进行拷贝。

例如直接将数组直接赋值给另一个数组,改变其中一个数组的值,另一个数组也会随之改变:

1
2
3
4
5
6
7
8
var arr = [1,2,3];
var arr2 = arr;
console.log(arr);  // [1, 2, 3]
console.log(arr2);  // [1, 2, 3]

arr2.push(4);
console.log(arr);  // [1, 2, 3, 4]
console.log(arr2);  // [1, 2, 3, 4]

数组的深拷贝(拷贝实例)

深拷贝也就是拷贝出一个新的实例,新的实例和之前的实例互不影响。

例如利用JSON.stringify和JSON.parse来实现深拷贝(简单):

JOSN对象中的stringify可以把一个js对象序列化为一个JSON字符串,parse可以把JSON字符串反序列化为一个js对象。

1
2
3
4
5
6
   var arr = [1,2,3];
   var arr2 = JSON.parse(JSON.stringify(arr));
   console.log(arr2);  //[1,2,3]
   arr2.push(4);
   console.log(arr);  //[1,2,3]
   console.log(arr2); // [1, 2, 3, 4]
1
2
3
4
5
6
7
8
9
   var arr = ['Hello', { number: 18 }];
   var arr2 = JSON.parse(JSON.stringify(arr));
   console.log(arr2);  // ['Hello',{ number:18 }];
   arr2[0] = 'World'
   console.log(arr);  // ['Hello',{ number:18 }];
   console.log(arr2);  // ['World',{ number:18 }];
   arr2[1].number = 20;
   console.log(arr);  // ['may',{ number:18 }];
   console.log(arr2);  // ['lee',{ number:20 }];

不管数组里面是基本类型(如 String, Number, Boolean )还是引用类型( Object ),两个数组都互不影响。但这个方式中,源对象的方法在拷贝的过程中丢失了,这是因为在序列化JavaScript对象时,所有函数和原型成员会被有意忽略,这个实现可以满足一些比较简单的情况,能够处理JSON格式所能表示的所有数据类型。

深拷贝其他方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function clone(Obj) {
  var buf;
  if (Obj instanceof Array) {
    buf = []; //创建一个空的数组
    var i = Obj.length;
    while (i--) {
      buf[i] = clone(Obj[i]);
    }
    return buf;
  } else if (Obj instanceof Object) {
    buf = {}; //创建一个空对象
    for (var k in Obj) {
      //为这个对象添加新的属性
      buf[k] = clone(Obj[k]);
    }
    return buf;
  } else {
    //普通变量直接赋值
    return Obj;
  }
}
1
2
3
4
5
6
7
Object.prototype.clone = function(){
	var o = this.constructor === Array ? [] : {};
    for(var e in this){
      o[e] = typeof this[e] === "object" ? this[e].clone() : this[e];
    }
	return o;
}

关于concat、slice以及扩展运算符…的拷贝程度

在JavaScript中,对于Object和Array这类引用类型值,当从一个变量向另一个变量复制引用类型值时,这个值的副本其实是一个指针,两个变量指向同一个堆内存,改变其中一个变量,另一个也会受到影响。

concat、slice的拷贝是浅拷贝,但网上有些文章却说这两个可以实现深拷贝,误人了。

1
2
3
4
5
6
7
8
9
10
11
12
13
var arr = ['Hello',{ number:18 }];
var arr2 = arr.slice(0);
// 或是用 concat 方式
// var arr2 = arr.concat();
console.log(arr2); //['Hello',{ number:18 }];

arr2[0] = 'World' 
console.log(arr); // ['Hello',{ number:18 }];
console.log(arr2);  // ['World',{ number:18 }];

arr2[1].number = 20;
console.log(arr); // ['Hello',{ number:20 }];
console.log(arr2); // ['Hello',{ number:20 }];

对于 slice 和 concat,它们都不会修改原数组,而是返回一个浅拷贝的新数组,原数组的元素会按照下述规则拷贝:

如果该元素是个对象引用(不是实际的对象),则会拷贝这个对象引用到新的数组里。两个对象引用都引用了同一个对象。如果被引用的对象发生改变,则新的和原来的数组中的这个元素也会发生改变。

对于基本类型字符串、数字及布尔值来说(不是 String、Number 或者 Boolean 对象),则会拷贝这些值到新的数组里。在别的数组里修改这些字符串或数字或是布尔值,将不会影响另一个数组。

最后,ES6的扩展运算符...实现的也是浅拷贝,同样的道理,一个数组中的引用类型改变,另一个数组中的引用类型也会跟着改变。

1
2
3
4
5
6
7
8
9
10
11
var arr = ['Hello',{ number:18 }];
var [ ...arr2 ] = arr;
console.log(arr2); //['Hello',{ number:18 }];

arr2[0] = 'World' 
console.log(arr); // ['Hello',{ number:18 }];
console.log(arr2);  // ['World',{number:18}]

arr2[1].age=20;
console.log(arr); // ['Hello',{ number:20 }];
console.log(arr2); // ['Hello',{ number:20 }];

参考资料: