深入剖析 JavaScript 的深复制

474 查看

一年前我曾写过一篇 Javascript 中的一种深复制实现,当时写这篇文章的时候还比较稚嫩,有很多地方没有考虑仔细。为了不误人子弟,我决定结合 Underscore、lodash 和 jQuery 这些主流的第三方库来重新谈一谈这个问题。

第三方库的实现

讲一句唯心主义的话,放之四海而皆准的方法是不存在的,不同的深复制实现方法和实现粒度有各自的优劣以及各自适合的应用场景,所以本文并不是在教大家改如何实现深复制,而是将一些在 JavaScript 中实现深复制所需要考虑的问题呈献给大家。我们首先从较为简单的 Underscore 开始:

Underscore —— _.clone()

在 Underscore 中有这样一个方法:_.clone(),这个方法实际上是一种浅复制 (shallow-copy),所有嵌套的对象和数组都是直接复制引用而并没有进行深复制。来看一下例子应该会更加直观:

让我们来看一下 Underscore 的源码

如果目标对象是一个数组,则直接调用数组的slice()方法,否则就是用_.extend()方法。想必大家对extend()方法不会陌生,它的作用主要是将从第二个参数开始的所有对象,按键值逐个赋给第一个对象。而在 jQuery 中也有类似的方法。关于 Underscore 中的 _.extend() 方法的实现可以参考 underscore.js #L1006

Underscore 的 clone() 不能算作深复制,但它至少比直接赋值来得“深”一些,它创建了一个新的对象。另外,你也可以通过以下比较 tricky 的方法来完成单层嵌套的深复制:

jQuery —— $.clone() / $.extend()

在 jQuery 中也有这么一个叫 $.clone() 的方法,可是它并不是用于一般的 JS 对象的深复制,而是用于 DOM 对象。这不是这篇文章的重点,所以感兴趣的同学可以参考jQuery的文档。与 Underscore 类似,我们也是可以通过 $.extend() 方法来完成深复制。值得庆幸的是,我们在 jQuery 中可以通过添加一个参数来实现递归extend。调用$.extend(true, {}, ...)就可以实现深复制啦,参考下面的例子:

在 jQuery的源码 – src/core.js #L121 文件中我们可以找到$.extend()的实现,也是实现得比较简洁,而且不太依赖于 jQuery 的内置函数,稍作修改就能拿出来单独使用。

lodash —— _.clone() / _.cloneDeep()

在lodash中关于复制的方法有两个,分别是_.clone()_.cloneDeep()。其中_.clone(obj, true)等价于_.cloneDeep(obj)。使用上,lodash和前两者并没有太大的区别,但看了源码会发现,Underscore 的实现只有30行左右,而 jQuery 也不过60多行。可 lodash 中与深复制相关的代码却有上百行,这是什么道理呢?