深拷贝和浅拷贝
一、基本类型和引用类型
-
基本数据类型(值类型):String 字符串、Number 数值、Boolean 布尔值、Null 空值、Undefined 未定义。
-
引用数据类型(引用类型):Object 对象。
注意:内置对象 Function、Array、Date、RegExp、Error等都是属于 Object 类型。也就是说,除了基本数据类型之外,其他的,都称之为 Object类型。
二、浅拷贝和深拷贝
如图所示:
- obj2是对obj1的浅拷贝,obj2新建了一个对象,但是obj2对象复制的是obj1的指针,也就是obj1的堆内存地址,而不是复制对象本身。obj1和obj2是共用了内存地址的。
- obj3是对obj1的深拷贝,obj3和obj1不共享内存。
概念:
- 浅拷贝 :只复制指向某个对象的指针,而不复制对象本身,相当于是新建了一个对象,该对象复制了原对象的指针,新旧对象还是共用一个内存块
- 深拷贝:是新建一个一模一样的对象,该对象与原对象不共享内存,修改新对象也不会影响原对象
三、赋值与浅拷贝
1.赋值
当我们把一个对象赋值给一个变量的时候,赋值的其实是该对象的栈内存地址而不是堆内存数据。也就是赋值前的对象和赋值后的对象两个对象共用一个存储空间(赋值的是栈内存地址,而该地址指向了同一个堆内存空间),所以,无论哪个对象发生改变,改变的都是同一个堆堆内存空间。因此,无论修改哪个对象对另一个对象都是有影响的。
var objA ={ name:'张三', age:8, pal:['王五','王六','王七'] } var objB = objA objB.name = '李四' objB.pal[0] = '王麻子' console.log('objA.name',objA.name) //李四 console.log('objB.name',objB.name)//李四 console.log('objB.pal',objB.pal)//['王麻子','王六','王七'] console.log('objA.pal',objA.pal)//['王麻子','王六','王七']
总结:赋值后的对象objB改变,原对象objA的值也改变,因为赋值后的对象objB赋值的是原对象objA的栈内存地址,他们指向的是同一个堆内存数据,所以对赋值后的对象objB对数据进行操作会改变公共的堆内存中的数据,所以原对象的值也改变了。
2.浅拷贝
浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原对象属性值的一份精准拷贝,如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。
有点抽象,来看个例子,该例子也是手写浅拷贝的方法。
var obj1 ={ name:'张三', age:8, pal:['王五','王六','王七'] } var obj3 = shallowCopy(obj1) function shallowCopy (src){ var newObj = {}; for(var prop in src ){ console.log(prop) if(src.hasOwnProperty(prop)){ newObj[prop] = src[prop] } } return newObj } obj3.name = '李四' obj3.pal[0] = '王麻子' console.log("obj1", obj1); //{age: 8, name: "张三", pal: ['王麻子', '王六', '王七']} console.log("obj3", obj3); //{age: 8, name: "李四", pal: ['王麻子', '王六', '王七']}
obj3改变了基本类型的值name,并没有使原对象obj1的name改变,obj3改变了引用类型的值,导致原对象的值也改变了。
总结:
- 赋值:就是对原对象的栈内存地址进行复制
- 浅拷贝:是对原对象的属性值进行精准复制,如果原对象的属性值是基本类型,那就是值的引用,所以浅拷贝后修改基本类型不会修改到原对象的,如果原对象属性值是引用类型,那么就是对引用类型属性值的栈内存的复制,所以修改引用类型属性值的时候会修改到原对象。
- 因此一般对无引用类型的属性的兑现拷贝的时候使用浅拷贝就行,对复杂对象包含引用类型属性的时候使用深拷贝。
四、浅拷贝的实现方式
1.Object.assign()
对于Object.assign()而言, 如果对象的属性值为简单类型(string, number),得到的新对象为‘深拷贝’;如果属性值为对象或其它引用类型,那对于这个对象而言其实是浅拷贝的。
var obj1 = { name: '张三', age: '25', info: { address: '北京市朝阳区', phone: '13888888888' } }; var obj2 = Object.assign({}, obj1); obj2.name = '李四'; obj2.info.phone = '13999999999'; console.log(obj1, 'obj1'); console.log(obj2, 'obj2');
注:Object.assign({}, obj1, obj2);
对于obj1和obj2之间相同的属性是直接覆盖的,如果属性值为对象,是不会对对象之间的属性进行合并的。
2、es6的对象展开运算符
展开运算符使用的对象如果只是针对简单的一级基础数据,就是深拷贝;使用的对象内容包含二级或更多的复杂的数据,那就是浅拷贝;
let a = { name: '张三', age: '18', info: { mobile: '17521315204', addres: '小张村' } } let b = { ...a }; b.name = '李四'; b.info.mobile = '18888888888'; console.log('a:', a); console.log('b:', b);
3、对于数组的浅拷贝:slice()和concat()
使用slice、concat方法并不是真正的深拷贝,它们只会深拷贝第一层,而到了第二层及其之后,只会复制引用地址了!
五、深拷贝的实现
1、通过JSON序列化实现深拷贝
序列化后再反序列化。
JSON.parse(JSON.stringify(obj))
注意,这种方法容易出很多问题,实际并不常用!
- 如果obj里有RegExp、Error对象,则序列化的结果将只得到空对象。
- 如果obj里面有时间对象,则序列化之后时间会是字符串的形式,而不是时间对象。
- 如果obj里有函数,undefined,则序列化的结果会把函数或 undefined丢失。
- 如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null。
- 无法处理function,无法处理循环引用对象。
2、lodash函数库实现深拷贝
var abj={ a:1, b:2 } var copyobj = lodash.cloneDeep(abj);
3、手写深拷贝
const deepClone = (obj) => { // 是对象吗?是就新建对象开始复制 if (typeof obj === 'object' && obj != null) { // 是对象,我们进一步确定是数组还是{} const newObj = Array.isArray(obj) ? [] : {}; for (let i in obj) { // 不管是不是对象,直接递归,外面的typeof会帮我们做判断是否要继续遍历 newObj[i] = deepClone(obj[i]); }; return newObj; } else { // 不是对象?直接返回 return obj; } };
测试一下:
// 测试 const obj = { name: '听风', age: 29, other: { gender: 'male', arr: [1, 2, 3] } }; const o = deepClone(obj); obj.other.gender = null; obj.other.arr[0] = 5; console.log(obj, o)
打印结果:
递归方法实现深度克隆原理:
- 遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝。
- 先判断各个字段类型,然后用递归解决嵌套数据。 判断拷贝的要进行深拷贝的是数组还是对象,是数组的话进行数组拷贝,对象的话进行对象拷贝。 进行深拷贝的不能为空,并且是对象或者是数组。