浅拷贝和深拷贝只针对于像Object,Array这样的复杂对象。之所以会出现深浅拷贝,实质上是因为在JavaScript当中基础类型和引用类型的不同存储方式,可见上篇。由于对对象的操作实际上是在操作对象的引用,所以在复制一个引用类型时,复制的值并不是对象本身,其实也是对象的引用,即指向该对象的指针。

1 | // 引用类型的复制 |
可以看出,复制的了我的人,复制不了我的心,由于me是个引用类型,在me改变内部属性时,myCopy中的属性也会也都会被改变。
浅拷贝
浅拷贝只复制对象内的第一层属性
,结果对象可能会与源对象还保持着联系。
1 | let me = { name: '张瑞', hobbies: ['游戏','旅游'] } |
由于只拷贝了第一层属性,在me改变name这个基本类型的属性时,并不会影响myCopy中的name属性。而改变hobbies这个引用类型的属性时,myCopy中的hobbies也被改变了,这就是浅拷贝:只复制对象内的第一层属性
。
ES6中新增的Object.assign方法便是一个浅拷贝的实践。用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)
。
通过浅拷贝得到的myCopy的hobbies属性和me的hobbies属性在内存中指向同一个地址,如果想完全的复制一个没有关联的我,这显然并不是期望得到的效果。所以,也就有了 深拷贝。
深拷贝
深拷贝可以对对象内的属性
进行递归复制,拷贝后的新对象与源对象完全断开了联系。
1 | let me = { name: '张瑞', hobbies: { game:['LOL','吃鸡'], other:[] },like:{} } |
不难看出,经过递归后,deepCopy完全的复制了另一个我。
在真正投入使用的时候,由于递归的特性,也会造成很多副作用,如对象环的问题(对象的某个属性值是对象本身),当递归调用次数足够大,就会造成栈溢出。下面有解决办法
其实还有更多的方法,如下
Reflect
Reflect 其实类似于 上文中的 for in
写法。使用Reflect.ownKeys
1 | // 判断目标是否是对象 |
lodash jQuery.extend
lodash 中的 cloneDeep
等api更加完善,具体可以参考lodash的baseClone等方法
1 | let copy = _.cloneDeep(me) |
jQuery.extend
也是非常经典的一个方法,用法多样。可以查看源码jQuery.extend
1 | let object1 = { |
JSON 序列化反序列化
这可能也是最简单也是最好理解的方法了,但是在实际日常使用中也会出现一些问题,那是因为JSON语言中并没有undefined,function
等数据类型,在拷贝具有这些类型的对象时,也必然会报错。
1 | function deepCopy(obj) { |
特殊情况
对象环问题
1 | let obj1 = {} |
对于这种问题,我们可以创建一个变量,在递归时把每个要拷贝的属性都缓存进来,下一轮递归的时候,如果缓存中有存在相同的对象,就可以直接使用这个对象并停止递归。这样改造之后,便不会在进入递归死循环,也就不会发生栈溢出了。
1 | function deepCopy(obj) { |
经过改造,没有再报栈溢出的错误,也得到了我们想要的结果。
当然,我们也可是使用WeakMap的方式创建一个更有趣的方法。
1 | function deepCopy(obj, map=new WeakMap()){ |
WeakMap的键名所指向的对象,不计入垃圾回收机制。WeakMap的设计目的在于,有时我们想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。
由于WeakMap的特性,它的键是弱引用的,正好符合现在的需求。
lodash也能处理对象环问题,具体可参见源码baseClone#L198。
拷贝原型上的属性
由于在JavaScript
中,对象是基于原型链设计的,所以某个属性查找不到时会沿着它的原型链向上查找
。
1 | function deepCopy(obj) { |

经过测试,使用Object.create
生成出来的create
对象,使用for...in
方法的拷贝,已经拷贝成功。
因为原形链上的属性也不会被追踪以及复制
,Object.keys、Reflect.ownKeys、JSON
方法本身也不会追踪原型链上的属性,所以使用这些方法并不能拷贝到原型上的属性.
拷贝 Symbol
因为Symbol
是一种特殊的数据类型,由于它的特点便是独一无二,所以此时,使用浅拷贝等于深拷贝。
但是此时,如果使用上文中使用for...in
的方法来拷贝时,是无法拷贝的,因为现在还没有对Symbol
类型做处理。
更多信息可参见阮大的教程Symbol-属性名的遍历
1 | function deepCopy(obj) { |
由于Reflect.ownKeys可以直接获取Symbol
值,所以Reflect的方法可以直接拷贝。
不可枚举的属性
当拷贝一些描述符属性、getter/setter一类不可枚举的属性时,就需要进一步的处理了。因为上面所写的种种方法,都无法拷贝这些属性(枚举不出来,臣妾做不到啊)。
我们现设置一个不可枚举的对象
1 | let me = { name: '张瑞', hobbies: { game:['LOL','吃鸡'], other:[] },like:{} } |
该实现方法了,恩。。。
不可枚举的属性那可咱办呢。
好吧,还是要看阮大的教程。
对象的扩展-属性的可枚举性和遍历
Object.getOwnPropertyDescriptorsES5 的Object.getOwnPropertyDescriptor()方法会返回某个对象属性的描述对象(descriptor)。ES2017 引入了Object.getOwnPropertyDescriptors()方法,返回指定对象所有自身属性(非继承属性)的描述对象。
好了开写,这次来个究极版本的
1 | function isObject(val) { |
好啦,快试试。
1 | let myCopy = deepCopy(me) |
其实还搜到一个MDN上的方法有点难记。
1 | // 下面这个函数会拷贝所有自有属性的属性描述符 |
github上的一个方法https://github.com/Tommy-White/deeplyAssign/blob/master/src/index.ts