一直想梳理下工作中经常会用到的深拷贝的内容,然而遍览了许多的文章,却发现对深拷贝并没有一个通用的完美实现方式。因为对深拷贝的定义不同,实现时的edge case过多,在深拷贝的时候会出现循环引用等问题,导致JS内部并没有实现深拷贝,但是我们可以来探究一下深拷贝到底有多复杂,各种实现方式的优缺点,同时参考下常用库对其的实现。
引用类型
之所以会出现深浅拷贝的问题,实质上是由于JS对基本类型和引用类型的处理不同。基本类型指的是简单的数据段,而引用类型指的是一个对象,而JS不允许我们直接操作内存中的地址,也是不能操作对象的内存空间,所以,我们对对象的操作都只是在操作它的引用而已。
在复制时也是一样,如果我们复制一个基本类型的值时,会创建一个新值,并把它保存在新的变量的位置上。而如果我们复制一个引用类型时,同样会把变量中的值复制一份放到新的变量空间里,但此时复制的东西并不是对象本身,而是指向该对象的指针。所以我们复制引用类型后,两个变量其实指向同一个对象,改变其中一个对象,会影响到另外一个。
浅拷贝
如果我们要复制对象的所有属性都不是引用类型时,可以使用浅拷贝,实现方式是遍历并复制,后返回新的对象。
如上面所说,我们使用浅拷贝会复制所有引用对象的指针,而不是具体的值,所以使用时一定要明确自己的需求,同时,浅拷贝的实现也是简单的。
JS内部实现了浅拷贝,如Object.assign()
,其中个参数是我们终复制的目标对象,后面的所有参数是我们的即将复制的源对象,支持对象或数组,一般调用的方式为
深拷贝
如果我们需要复制一个拥有所有属性和方法的新对象,要用到深拷贝,JS并没有内置深拷贝方法,主要是因为:
- 深拷贝怎么定义?我们怎么处理原型?怎么区分可拷贝的对象?原生DOM/BOM对象怎么拷贝?函数是新建还是引用?这些edge case太多导致我们无法统一概念,造出大家都满意的深拷贝方法来。
- 内部循环引用怎么处理,是不是保存每个遍历过的对象列表,每次进行对比,然后再造一个循环引用来?这样带来的性能消耗可以接受吗。
解释一些常见的问题概念,防止有些同学不明白我们在讲什么。比如循环引用:
这样当我们深拷贝obj对象时,会循环的遍历b属性,直到栈溢出。 我们的解决方案为建立一个[]
,每次遍历对象进行比较,如果[]
中已存在,则证明出现了循环引用或者相同引用,我们直接返回该对象已复制的引用即可:
处理原型和区分可拷贝的对象:我们一般使用function.prototype
指代原型,使用obj.__proto__
指代原型链,使用enumerable
属性表示是否可以被for ... in
等遍历,使用hasOwnProperty
来查询是否是本身元素。在原型链和可遍历属性和自身属性之间存在交集,但都不相等,我们应该如何判断哪些属性应该被复制呢?
函数的处理:函数拥有一些内在属性,但我们一般不修改这些属性,所以函数一般直接引用其地址即可。但是拥有一些存取器属性的函数我们怎么处理?是复制值还是复制存取描述符?
这个是我们想要的结果吗?大部分场景下不是吧,比如我要复制一个已有的Vue对象。当然我们也有解决方案:
虽然边界条件很多,但是不同的框架和库都对该方法进行了实现,只不过定义不同,实现方式也不同,如jQuery.extend()
只复制可枚举的属性,不继承原型链,函数复制引用,内部循环引用不处理。而lodash实现的更为,它实现了结构化克隆算法
。 该算法的优点是:
- 可以复制 RegExp 对象。
- 可以复制 Blob、File 以及 FileList 对象。
- 可以复制 ImageData 对象。CanvasPixelArray 的克隆粒度将会跟原始对象相同,并且复制出来相同的像素数据。
- 可以正确的复制有循环引用的对象
依然存在的缺陷是:
- Error 以及 Function 对象是不能被结构化克隆算法复制的;如果你尝试这样子去做,这会导致抛出 DATA_CLONE_ERR 的异常。
- 企图去克隆 DOM 节点同样会抛出 DATA_CLONE_ERROR 异常。
- 对象的某些特定参数也不会被保留
- RegExp 对象的 lastIndex 字段不会被保留
- 属性描述符,setters 以及 getters(以及其他类似元数据的功能)同样不会被复制。例如,如果一个对象用属性描述符标记为 read-only,它将会被复制为 read-write,因为这是默认的情况下。
- 原形链上的属性也不会被追踪以及复制。
我们先来看看常规的深拷贝,它跟浅拷贝的区别在于,当我们发现对象的属性是引用类型时,进行递归遍历复制,直到遍历完所有属性:
这个的主要问题是不处理循环引用,不处理对象原型,函数依然是引用类型。上面描述过的复杂问题依然存在,可以说是简陋但是日常工作够用的深拷贝方式。
另外还有一种方式是使用JSON序列化,巧妙但是限制更多:
JSON是一种表示结构化数据的格式,只支持简单值、对象和数组三种类型,不支持变量、函数或对象实例。所以我们工作中可以使用它解决常见问题,但也要注意其短板:函数会丢失,原型链会丢失,以及上面说到的所有缺陷。
库实现
上面的两种方式可以满足大部分场景的需求,如果有更复杂的需求,可以自己实现。现在我们可以看一些框架和库的解决方案,下面拿经典的jQuery和lodash的源码看下,它们的优缺点上面都说过了:
jQuery.extend()
lodash _.baseClone()
参考资料
- 知乎 JS的深拷贝和浅拷贝: https://www.zhihu.com/questio…
- Javascript之深拷贝: https://aepkill.github.io/201…
- js对象克隆之谜:http://b-sirius.me/2017/08/26…
- 知乎 JS如何完整实现深度Clone对象:https://www.zhihu.com/questio…
- github lodash源码:https://github.com/lodash/lod…
- MDN 结构化克隆算法:https://developer.mozilla.org…
- jQuery v3.2.1 源码
- JavaScript程序设计 第4章(变量、作用域和内存问题)、第20章(JSON)
暂无评论内容