JS浅拷贝与深拷贝

往期笔记实例引入

  • 在ES6的解构赋值中我们提到过:使用扩展运算符复制对象是浅拷贝的。
  • Redux入门(1)中我们提到过:不能修改state,所以我们需要对state进行深拷贝成newState。

基本类型与引用类型

  • 数据分为基本数据类型(String, Number, Boolean, Null, Undefined,Symbol)和引用数据类型( Object,Array,Date,Function)。
  • 基本类型名字和值都会储存在栈内存中。
  • 引用类型名字存在 栈内存 中,值存在 堆内存 中,但是 栈内存 会提供一个 引用的地址 指向堆内存中的值(指针)。
    栈内存与堆内存

赋值和浅拷贝的区别

  • 当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。
  • 浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。
    • 如果属性是基本类型,拷贝的就是基本类型的.
    • 如果属性是引用类型,拷贝的就是内存地址.
    • 因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。

例子
对比赋值与浅拷贝会对原对象带来哪些改变:

1
2
3
4
5
6
7
8
9
10
11
// 对象赋值
var obj1 = {
'name' : 'zhangsan',
'age' : '18',
'language' : [1,[2,3],[4,5]],
};
var obj2 = obj1;
obj2.name = "lisi";
obj2.language[1] = ["二","三"];
console.log('obj1',obj1)
console.log('obj2',obj2)

对象赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 浅拷贝
var obj1 = {
'name' : 'zhangsan',
'age' : '18',
'language' : [1,[2,3],[4,5]],
};
var obj3 = shallowCopy(obj1);
obj3.name = "lisi";
obj3.language[1] = ["二","三"];
function shallowCopy(src) {
var dst = {};
for (var prop in src) {
if (src.hasOwnProperty(prop)) {
dst[prop] = src[prop];
}
}
return dst;
}
console.log('obj1',obj1)
console.log('obj3',obj3)

浅拷贝
上面例子中,obj1是原始数据,obj2是赋值操作得到,而obj3浅拷贝得到。我们可以很清晰看到对原始数据的影响,具体请看下表:
赋值与浅拷贝、深拷贝


深拷贝与浅拷贝

假设B复制了A,当修改A时,看B是否会发生变化,如果B也跟着变了,说明这是浅拷贝,如果B没变,那就是深拷贝
深拷贝与浅拷贝

  • 浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存
    • 浅拷贝:拷贝一个变量的时候,复制了栈内存,没有复制 堆内存
  • 深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
    • 深拷贝:拷贝一个变量的时候,复制了栈内存,同时也复制了 堆内存

深拷贝实现思路

在 堆内存 中也开辟一个新的内存专门为b存放值,就像基本类型那样,也就达到深拷贝的效果了。

浅拷贝
浅拷贝可以理解为就是复制一份来引用,所有引用对象都指向一份数据,并且都可以修改这份数据。 对于字符串类型,浅拷贝是对值的拷贝,对于对象来说,浅拷贝是对对象地址 的拷贝,也就是复制的结果是两个对象指向同一个内存地址,修改其中一个对象的属性,则另一个对象的属性也会改变。

深拷贝
深拷贝则是复制变量值,对于非基本类型的变量,则递归至基本类型变量后,再复制。深复制不同于浅复制,它会开辟新的内存地址,两个对象对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性。

深、浅拷贝的优缺点

  • 浅拷贝
    • 优点:拷贝速度快,占用内存空间小。
    • 缺点:如果你改变了对象B所指向的内存地址,你同时也会改变对象A指向这个地址的字段。
  • 深拷贝
    • 这种方式会完全拷贝所有数据,优点是B与A不会相互依赖(A,B完全脱离关联)
    • 缺点:拷贝的速度更慢,代价更大 (可理解为耗费了更多内存空间)。

对象的浅拷贝

Object.assign()

  • 语法:Object.assign(目标对象, ...源对象)
  • Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象
  • 但是 Object.assign() 进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。
  • 注意:当object只有一层的时候,是深拷贝
  • MDN文档
    1
    2
    3
    4
    5
    // 浅拷贝
    var obj = { a: {a: "kobe", b: 39} };
    var initalObj = Object.assign({}, obj);
    initalObj.a.a = "wade";
    console.log(obj.a.a); // wade

特别情况深拷贝

注意:当object只有一层的时候,是深拷贝

1
2
3
4
// 当object只有一层的时候,是深拷贝
const obj = { a: 1 };
const copy = Object.assign({}, obj);
console.log(copy); // { a: 1 }

数组的浅拷贝

slice和concat这两个方法,仅适用于对不包含引用对象的一维数组的深拷贝。对于包含引用对象的数组,这两个方法是浅拷贝(比如[{"name":"weifeng"},{"name":"boy"}]

Array.prototype.slice方法

  • 对于array对象的slice函数,返回一个数组的一段。(仍为数组)
  • 语法:arrayObj.slice(start, [end])
    • arrayObj 必选项。一个 Array 对象。
    • start 必选项。arrayObj 中所指定的部分的开始元素是从零开始计算的下标。
    • end 可选项。arrayObj 中所指定的部分的结束元素是从零开始计算的下标。
  • 说明
    • slice 方法返回一个 Array 对象,其中包含了 arrayObj 的指定部分。
    • slice 方法一直复制到 end 所指定的元素,但是不包括该元素。
    • 如果 start 为负,将它作为 length + start处理,此处 length 为数组的长度。
    • 如果 end 为负,就将它作为 length + end 处理,此处 length 为数组的长度。
    • 如果省略 end ,那么 slice 方法将一直复制到 arrayObj 的结尾。
    • 如果 end 出现在 start 之前,不复制任何元素到新数组中。

深拷贝与浅拷贝

  • 数组元素基本数据类型,比如String,Number,Boolean时,属于深拷贝
  • 当数组元素是引用数据类型,比如Object,Array,Function时,属于浅拷贝
    1
    2
    3
    4
    5
    var arr1 = ["1","2","3"];
    var arr2 = arr1.slice(0);
    arr2[1] = "9";
    console.log("数组的原始值:" + arr1 );//1 2 3
    console.log("数组的新值:" + arr2 );//1 9 3

Array.prototype.concat方法

  • concat() 方法用于连接两个或多个数组。该方法不会改变现有的数组,而仅仅会返回被连接数组的一个副本
  • 语法arrayObject.concat(arrayX,arrayX,......,arrayX)
  • 说明:返回一个新的数组。该数组是通过把所有 arrayX 参数添加到 arrayObject 中生成的。如果要进行 concat() 操作的参数是数组,那么添加的是数组中的元素,而不是数组。

深拷贝与浅拷贝

  • 当数组元素是基本数据类型,比如String,Number,Boolean时,属于深拷贝
    1
    2
    3
    4
    5
    var arr1 = ["1","2","3"];
    var arr2 = arr1.concat();
    arr2[1] = "9";
    console.log("数组的原始值:" + arr1 );// 1 2 3
    console.log("数组的新值:" + arr2 );//1 9 3
  • 当数组元素是引用数据类型,比如Object,Array时,属于浅拷贝
    1
    2
    3
    4
    5
    var arr1 = [{a:1},{b:2},{c:3}];
    var arr2 = arr1.concat();
    arr2[0].a = "9";
    console.log("数组的原始值:" + arr1[0].a ); // 数组的原始值:9
    console.log("数组的新值:" + arr2[0].a ); // 数组的新值:9

二维数组/对象 通用深拷贝方法

递归拷贝所有层级属性

  • 原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝。
  • 复习
    • typeof引用类型(数组、对象)都判断为object
    • (更多for-infor-of可参考《ES6扩展 对象扩展》
      • 深拷贝不使用for-of的原因: 数组元素/对象属性为不可迭代的对象时会报错。
      • for-in主要用于对 对象属性 进行遍历for-in 循环中的代码每执行一次,就会对 对象的属性 进行一次操作。
        • for(key in obj)中,key是属性名。(注意:in操作符针对数组时,key是数组下标)
        • 对象/数组/字符串 都有属性继承自原型。使用for-in遍历时会遍历到继承属性,所以需要提前筛选。Object的hasOwnProperty()方法返回一个布尔值,判断对象是否包含特定的自身(非继承)属性。(对数组使用for-of时不需判断,例子可看MDN注意数组也是会有继承属性的
      • for-of主要用于对 数组元素 进行遍历,不使用for-in是因为for-in无法保证 索引顺序 ,所以使用for-of或者forEach()遍历数组会更加合理。
        • 不使用for-of的原因: 数组元素/对象属性为不可迭代的对象时会报错。
        • for(key of arr)中,key是数组元素。(不同于for(key in arr)中key是数组下标)
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
/**
* 深拷贝方法1
* @param {Object} obj 要拷贝的对象
*/
function deepClone(obj) {
// 如遇值类型则直接返回,否则继续执行【注意Object有引号】
if (typeof obj !== "object" || obj === null) {
return obj;
}

// 初始化返回结果
let cloneObj = Array.isArray(obj) ? [] : {};

// 遍历对象属性/数组元素
for (key in obj) {
// 筛选对象自身属性
if (obj.hasOwnProperty(key)) {
// 递归调用deepClone(),
cloneObj[key] = deepClone(obj[key])
}
}

// 返回结果
return cloneObj;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 深拷贝方法1优化
* 用Object.keys()代替for-in和hasOwnProperty()
* 使用Object.keys()获取非原型属性,避免了复制原型属性的问题
* @param {Object} obj 要拷贝的对象
*/
function deepClone(obj) {
if (typeof obj !== "object" || obj === null) {
return obj;
}

let cloneObj = Array.isArray(obj) ? [] : {};

Object.keys(obj).forEach(key => {
cloneObj[key] = deepClone(obj[key]);
});

return cloneObj;
}
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
29
30
31
/**
* 深拷贝方法2
* 写的不是很好,层层嵌套,不简洁,用上面的比较好
* @param {Object} obj 要拷贝的对象
*/
function deepClone(obj){
// 根据传入的参数obj判断拷贝数组还是对象
let objClone = Array.isArray(obj)?[]:{};
// 判断参数obj是否是引用类型(数组/对象)
if(obj && typeof obj==="object"){
// 是对象则用for in遍历 对象属性
for(key in obj){
// 如果是非继承属性
if(obj.hasOwnProperty(key)){
//判断ojb子元素(数组元素/对象属性)是否为引用类型(数组、对象、函数)【注意Object有引号】
if(obj[key]&&typeof obj[key] ==="object"){
//是则递归复制
objClone[key] = deepClone(obj[key]);
}else{
//如果不是,简单复制
objClone[key] = obj[key];
}
}
}
}
return objClone;
}
let a=[0, 1, null, [1, 2], { name: "a" }, function a() { return 1; }],
b=deepClone(a);
b[3][0] = 5;
console.log(a,b);

结果


JSON.parse(JSON.stringify())

  • 这也是我在Redux入门(1)中对state进行深拷贝成newState时使用的方法。(由于这里的state对象中没有方法,所以可以使用JSON进行深拷贝)
  • 原理:不拷贝引用对象,使用stringify()拷贝一个 JSON字符串 会新辟一个新的存储地址,这样就切断了引用对象的指针联系
  • 缺点:无法实现对 数组/对象中 方法 的深拷贝,会显示为undefined。
    • 如果是对象第一层属性中含有方法,则可以使用ES6提供的object.assign()进行对象方法的拷贝
      • 注意:仅限对象第一层属性的方法,因为assign是浅拷贝的,第二层属性的方法就只拷贝引用地址了。
      • assign处理数组的时候,会把数组视为对象,返回的是对象

使用assign可解决JSON拷贝的缺点,拷贝对象第一层属性中的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let obj1 = {
a: 0,
b: { c: 0 },
d: function(){
return 1;
}
};
//使用JSON拷贝不到方法
let obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj1,obj2);
//使用assign可以拷贝到第一层属性的方法
let obj3 = Object.assign({}, obj1);
obj3.d=function(){
return 2;
};
console.log(obj1,obj3);

结果
assign仅限拷贝对象第一层属性的方法,因为assign是浅拷贝的,第二层属性的方法就只拷贝引用地址了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let obj1 = {
a: 0,
b: { c: 0 },
d: {
e: function () {
return 1;
}
}
};
let obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj1, obj2);
let obj3 = Object.assign({}, obj1);
obj3.d.e = function () {
return 2;
};
console.log(obj1, obj3);

结果

例子:

1
var 新对象 = JSON.parse(JSON.stringify(老对象));

stringify()将 JS对象 转换为 JSON字符串
parse()将 JSON字符串 转换为 JS对象

1
2
3
4
5
6
7
8
9
10
var target = [0, 1, null, [1, 2], { name: "a" }, function a() { return 1; }]

// 完成下面的深拷贝函数
function deepCopy(obj) {
return JSON.parse(JSON.stringify(obj));
}
var objClone = deepCopy(target);
objClone[4].name = "zzz";
console.log(target);
console.log(objClone);

无法实现对 数组/对象中 方法 的深拷贝


函数库lodash

该函数库也有提供 _.cloneDeep 用来做 Deep Copy。

1
2
3
4
5
6
7
8
9
var _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);
// false

扩展运算符的深、浅拷贝

  • 当value是基本数据类型,比如String,Number,Boolean时,是可以使用拓展运算符进行深拷贝
  • 当value是引用类型的值,比如Object,Array,引用类型进行深拷贝也只是拷贝了引用地址,所以属于浅拷贝
    1
    2
    3
    4
    var car = {brand: "BMW", price: "380000", length: "5米"}
    var car1 = { ...car, price: "500000" }
    console.log(car1); // { brand: "BMW", price: "500000", length: "5米" }
    console.log(car); // { brand: "BMW", price: "380000", length: "5米" }

,