Javascript的深浅拷贝
基本数据类型与引用数据类型
在讨论深浅拷贝之前,我们需要先说一下基本数据类型和引用数据类型:
- 基本数据类型:直接存储在栈中的数据
- 引用数据类型:存储的是该对象在栈中的引用,真实数据存放在堆内存中。
因为基本数据类型每次复制都会在栈中新开辟一块内存存放值,所以基本数据类型并不会涉及到深浅拷贝,接下来关于深浅拷贝到讨论仅针对于像Object和Array这样的引用数据类型。
浅拷贝与深拷贝
首先说一下什么是深浅拷贝:
- 浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。
- 深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,对新对象进行操作不会影响到原对象。
简单说就是有一个对象A,还有一个对象B是通过复制A来实现的,如果修改了对象A,对象B也跟着一起发生了变化,这就是浅拷贝;当修改了对象A,但是对象B不受影响,这就是深拷贝。
浅拷贝与赋值
赋值:
- 把一个对象赋值给一个新的变量时,赋的是对象在栈中的地址,而不是堆中的数据,所以两个对象是指向同一个存储空间的。因此无论是哪个对象发生了改变,都会改变存储空间中的数据,另一个对象也会随之发生改变,即两个对象是联动的。
- 赋值的对象发生任何改动都会对原数据产生影响。
浅拷贝:
- 浅拷贝是按位拷贝对象,他会创建一个有着原始对象属性值精确拷贝的新对象。如果原始对象是基本数据类型,就会拷贝栈中的值,如果原始对象是引用数据类型,就会拷贝栈中存储的内存地址。
- 对浅拷贝的对象进行改动,如果原始对象是基本数据类型则不受影响,如果原始对象是引用数据类型则会随着进行变化。
下面说一些常用的深浅拷贝的方法(有些方法来自前辈的文章,如有侵权请联系删除)。
浅拷贝的实现方法
Object.assign()方法
关于Object.assign()方法,引用MDN的解释:
Object.assign()方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。该方法将返回目标对象。语法:
Object.assign(target, ...sources)
该方法拷贝的是可枚举属性值,所以如果源值是一个对象的引用,它仅仅会复制其引用值
1 | let obj = {a: 1, b: 2} |
该方法也可用于实现对象的合并,不过要注意属性会被后续参数中具有相同属性的其他对象覆盖。
关于该方法的更多使用及细节,这里不展开说,有兴趣可以参阅MDN。
Array.prototype.concat()方法
concat()方法用于合并两个或多个数组,此方法不会更改现有的数组,而是返回一个新数组。
不难看出,这个方法仅使用于数组。
1 | let arr = [1, 3, [5]] |
看到这里大家应该能更加清楚浅拷贝与赋值的区别了吧,咱们在看一下赋值的操作会怎样:
1 | let arr = [1,2,[5]] |
从这里应该不难看出了吧,回想一下赋值与浅拷贝的定义,我们也能清楚的知道,赋值的引用数据类型,进行任何改动都会互相影响,其存储的是整个源对象的内存地址;而浅拷贝的对象,是对源对象的每个属性值进行精确拷贝,所以对源对象中的基本数据类型进行更改,不会影响到拷贝对象,只有更改引用数据类型才会因为存储了相同的内存地址而互相影响,看一下上面的例子应该比较容易理解了,这也是我当初犯糊涂的一个地方。
Array.prototype.slice()方法
slice()方法返回一个新的数组对象,这一对象是一个由begin和end决定的原数组的浅拷贝(包括begin,不包括end)。原始数组不会被改变。
begin与end均为可选值,省略begin则从头开始,省略end则一直提取到数组末尾
在MDN到定义中已经清楚的写出了slice()方法返回的是原数组的浅拷贝,这里不过多赘述。
1 | let arr = [1, 2, 3] |
再次强调concat()与slice()方法
slice()和concat()方法并不会修改原数组,只是返回原数组中的浅拷贝。- 原数组中的元素按照下述规则进行拷贝:
- 如果元素是基本数据类型,会将类型值直接拷贝到新数组,两个数组对于这些元素的改动不会互相影响。
- 如果元素是引用数据类型,会将该数据的内存地址拷贝到新数组,两个数组对于这些元素的操作会产生互相影响。
深拷贝的实现方法
JSON.parse(JSON.stringify())
先将源对象转换为json字符串在解析为js对象
1 | let arr = [1, 2, [5]] |
注意:该方法不能用来处理函数
手写递归方法
原理是递归遍历源对象的所有引用类型,知道其是基本数据类型再去进行复制。
在进行实现之前先介绍一下toString方法,我们可以通过toString.call()来判断数据类型:
仅限于Object.prototype.toString方法,其他类型的toString方法或多或少进行了重写,所以只有Object原型上的toString方法有这个效果,下边的toString指的是Object.prototype.toString。所以需要使用call方法来获取指定数据的原型。
1 | toString.call(3) // '[object Number]' |
可以发现,不管是什么类型得到的都是以[object开头,所以我们只需要下标8到-1的部分就可以得到数据的完整类型
下面是深拷贝的实现:
1 | // 深拷贝的函数实现 |
Javascript的深浅拷贝