浅拷贝与深拷贝
本文最后更新于:1 年前
浅拷贝与深拷贝
在了解深浅拷贝之前应该了解JavaScript的数据类型
数据类型
数据类型分为基本数据类型(String, Number, Boolean, Null, Undefined, Symbo, Symbol (ES6), BigInt (ES2020))和引用数据类型 (Object、Array、Function)。
基本数据类型的特点:直接存储在栈(stack)中的数据
引用数据类型的特点:存储的是该对象在栈中引用,真实的数据存放在堆内存里
引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。
当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
深拷贝和浅拷贝的定义
深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的。
- 浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
- 深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。
深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
赋值
当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。
赋值是将某一数值或对象赋给某个变量的过程,分为下面 2 部分
- 基本数据类型:赋值,赋值之后两个变量互不影响
- 引用数据类型:赋址,两个变量具有相同的引用,指向同一个对象,相互之间有影响
对基本类型进行赋值操作,两个变量互不影响。
1 |
|
对引用类型进行赋址操作,两个对象指向的同一个内存地址,修改引用值会对另一个影响
1 |
|
浅拷贝
浅拷贝会重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响。
简单来说可以理解为浅拷贝只解决了第一层的问题,拷贝第一层的基本类型值,以及第一层的引用类型地址。
遍历对象,将每一个属性和值分别赋值给一个空对象。但是浅拷贝没有处理引用值
1 |
|
浅拷贝的问题
- 没有处理引用值,引用值还是指向同一个地址。
- 如果原型上还有其他属性,浅拷贝也会拷贝下来
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
32
33
34
35
36
37Object.prototype.num = 1
let person1 = {
name: '张三',
age: 18,
sex: 'male',
height: 180,
weight: 140,
son: {
first: 'Jenney',
},
}
let person2 = {}
//浅拷贝
for (let key in person1) {
person2[key] = person1[key]
}
person2.name = '李四'
person2.son.second = 'Lucy'
console.log(person1, person2)
// age: 18
// height: 180
// name: "张三"
// sex: "male"
// son: {first: 'Jenney', second: 'Lucy'}
// weight: 140
// [[Prototype]]: Object
// num: 1
// age: 18
// height: 180
// name: "李四"
// num: 1
// sex: "male"
// son: {first: 'Jenney', second: 'Lucy'}
// weight: 140
// [[Prototype]]: Object
// num: 1实现浅拷贝
用 for in 实现浅拷贝
传入拷贝源和目标对象,考虑排除原型上的对象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
26Object.prototype.num = 1
let person1 = {
name: '张三',
age: 18,
sex: 'male',
height: 180,
weight: 140,
son: {
first: 'Jenney',
},
}
//浅拷贝
let person2 = clone(person1)
person2.name = '李四'
person2.son.second = 'Lucy'
console.log(person1, person2)
//浅拷贝
function clone(origin, target) {
let tar = target || {}
for (let key in origin)
if (origin.hasOwnProperty(key)) {//排除原型,只打印自身属性
tar[key] = origin[key]
}
return tar
}
for in 方法做浅拷贝过于繁琐。ES6 给我们提供了新的语法糖,通过Object.assign()
(推荐)Object.assgin()
可以实现浅拷贝。1
2
3
4
5// 语法1
obj2 = Object.assgin(obj2, obj1);
// 语法2
Object.assign(目标对象, 源对象1, 源对象2...);
解释:将obj1 拷贝给 obj2。执行完毕后,obj2 的值会被更新。
作用:将 obj1 的值追加到 obj2 中。如果对象里的属性名相同,会被覆盖。
从语法2中可以看出,Object.assign()
可以将多个“源对象”拷贝到“目标对象”中。Object.assign()
方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。
但是 Object.assign()
进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身
1 |
|
注意:当object只有一层的时候,是深拷贝
展开运算符...
展开运算符是一个 es6 / es2015特性,它提供了一种非常方便的方式来执行浅拷贝,这与 Object.assign ()
的功能相同。
1 |
|
深拷贝
深拷贝:从堆内存中开辟一个新的区域存放新对象,对象中的子对象进行递归拷贝,拷贝前后的两个对象互不影响。
1 |
|
实现深拷贝
递归
递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝。
1 |
|
有种特殊情况需注意就是对象存在循环引用的情况,即对象的属性直接的引用了自身的情况,解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。使用WeakMap代替Map,如果是WeakMap的话,就是弱引用关系,当下一次垃圾回收机制执行时,这块内存就会被释放掉。
JSON.parse(JSON.stringify())
(不推荐)
用JSON.stringify将对象转成JSON字符串,再用JSON.parse()把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。但是无法拷贝其他引用类型、拷贝函数、循环引用等情况
这种方法虽然可以实现数组或对象深拷贝,但不能处理函数和正则,因为这两者基于JSON.stringify和JSON.parse处理后,得到的正则就不再是正则(变为空对象),得到的函数就不再是函数(变为null)了。
1 |
|
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!