那么该对象在内存中的存放情况是这样子的:
可以看到,grade和student对象其实是两个不同的对象,他们拥有属于自己的内存空间,只不过student里面保留着对grade的引用。也就是说,grade对象并不是student自身的属性,属于student自身的属性的只有name,age,和grade的指针。那么到这里应该很容易就能想到:
Object.assign()只能拷贝源对象的首层属性,对于源对象里面嵌套的引用类型并不能复制。
Talk is cheap, show me the code:
深拷贝(deep copy) 算是js里面比较久经不衰的话题,论坛爱讨论,面试也爱考。何为深拷贝?其实就是实现对一个引用类型的完整复制。
什么是引用类型? js中有值类型和引用类型两种类型,基本类型就是像
number
,boolean
这些。值类型在定义的时候,会在栈内存为其分配一个固定的空间。 而引用类型比较特殊,它的大小是不固定的,所以在定义引用类型的时候,解析器会在堆内存为其分配空间,然后再在栈内存分配一个指向堆内存里该内存空间的指针。在内存中地址分配如图:
js中的引用类型有三种:
Array
Object
String
这种直接拷贝指针的方法通常叫做浅拷贝(shallow copy),接下来要讲的就是实现引用类型的深拷贝。
Array的深拷贝
对Array类型实现深拷贝还算是比较简单的,最常用的方法是用
slice
:当然如果Array里面嵌套了Array,slice肯定不行了,但是可以换种思路,利用递归解决:
漂亮的functional programming。
Object的深拷贝
真正的重点在Object类型的深拷贝。
1.最hack的方法:JSON.parse(JSON.stringify())
为什么说是最hack的方法呢,因为这种方法把一个object当成json对待,先转字符串,再又转回json,从而生成一个新object。 先抛开性能问题不说(对json进行转换的操作都非常耗时),这种方法有一个很大的缺点,就是要求转换的object一定要是标准的json格式,也就是说当object中含有
undefined
,null
,function
类型都无法进行转换。2.浅的深拷贝:Object.assign()
Object.assign()
方法是es6中提供的原生方法,用于对象的合并:Object.assign<T, U>(target: T, source: U): T & U
可以看到
Object.assign
接受两个参数,一个是目的对象,一个是源对象。利用Object.assign
合并两个对象:按照这种思路,我们可以用一个对象和一个空对象合并模拟拷贝的效果:
看起来目的是达到了,对象a的属性的修改并没有影响到对象b。但是事情并没有这么简单。 查阅一下MDN,发现对
Object.assign
有这样一段描述:里面说到了一些关键的地方:only copies own properties,也就是只拷贝对象自身的属性。这意味着什么呢?假如有这样一个对象:
那么该对象在内存中的存放情况是这样子的:
可以看到,
grade
和student
对象其实是两个不同的对象,他们拥有属于自己的内存空间,只不过student
里面保留着对grade
的引用。也就是说,grade
对象并不是student
自身的属性,属于student
自身的属性的只有name
,age
,和grade
的指针。那么到这里应该很容易就能想到:Object.assign()
只能拷贝源对象的首层属性,对于源对象里面嵌套的引用类型并不能复制。Talk is cheap, show me the code:
果然MDN没有骗我。
3.最接近完美的方法:传统递归
既然用以上的方法都不完美,那么是否可以回归淳朴,直接手写递归解决?
答案当然是可以的,而且递归是最接近完美的方法,一层一层深入拷贝,思路跟深拷贝数组基本一样:
这种递归拷贝,应该是最接近完美的方法了。但是,事情还是没有这么简单,因为我说了这是最接近完美,而不是最完美。
因为这种方法没有考虑引用环的情况。
什么鬼!?什么是引用环?请看下面的情况:
内存情况如图所示:
可以看到,a中的属性b中的属性a又引用了a自身,形成了一个环,这种就叫引用环,但是这种逻辑完全又是合法的(参考数据结构中的循环链表)。如果对一个含有引用环的对象进行递归拷贝,就会出现栈溢出的现象(因为递归没法终止)。
当然,在实际开发中,出现引用环的情况其实很少很少,而且也要尽量避免出现,所以说递归拷贝足够应对大多数场景了。
一些思考
故事到这里基本就结束了,但是有一些有意思的问题还是可以思考一下。之前刷知乎,看见有人在讨论:
深拷贝一个对象究竟要不要拷贝对象的方法和对象的proto
额,我自己认真想了一下,我的答案(不一定是对的,只是个人认为)是:两个都不需要,理由如下:
对于
function
:首先,你根本没有方法深拷贝一个function
,其次,也根本不需要深拷贝一个function
。什么是function
,就是对某些逻辑集合的抽象嘛,为什么要有function
?就是为了代码复用。说到底,function
不是数据,只是一个处理数据的工具。数据需要copy,工具不需要copy。对于
_proto_
:一个对象被创造出来后,其实已经跟他的_proto_
关系不大了。我们在日常开发当中,基本不需要操作一个对象的_proto_
,而且无论是es6还是typescript,很明显js的发展也是朝着去prototype
拥抱class
这个方向在走,对象的_proto_
的概念已经被弱化。而且,这样浪费内存真的好吗。---EFO---