JavaScript中如何实现深度克隆

2018-06-13  本文已影响0人  指尖的宇宙
js.png

一:为什么要实现深度克隆?

这是一个前端面试经常问到的问题,并且在知乎上我看到很多的前端大神也都探讨过。这个问题背后的考察点相当丰富,涉及JS的数据类型、数据存储、内存管理。还涉及很多边界条件的考虑,很具有代表性。所以为了巩固这个这些知识点,查阅了很多资料,整理一篇文章,供学习交流使用,如有不足之处,欢迎指正。

二:JavaScript中的内存管理

JS内存管理,往深了挖很复杂,这里只做简单的介绍,帮助理解js的基本类型和引用类型,为了后面讲解深度克隆做铺垫,我们知道JS拥有自动的垃圾回收机制,这样就使得很多前端开发人员不是很重视内存管理这一块。但是其实这一部分的内容对于理解JS中原型与原型链,闭包,递归都是非常有帮助的。

在JS中,每一个数据都需要一个内存空间。内存空间又被分为两种:

栈内存(stock)
堆内存(heap)
乒乓球盒子.png

乒乓球的存放方式与栈内存中存储数据的方式如出一辙。处于盒子中最顶层的乒乓球,它一定是最后被放进去的,但可以最先被使用。而我们想要使用底层的乒乓球,就必须将上面的两个乒乓球取出来,让最底层的乒乓球处于盒子顶层。这就是栈空间 “先进后出,后进先出” 的特点。

与java等其他语言不同,JS的引用数据类型,比如数组Array,它们值的大小是不固定的,可以再不声明长度的情况下,动态填充。引用数据类型的值是保存在堆内存中的对象。

JavaScript不允许直接访问堆内存中的位置,因此我们不能直接操作对象的堆内存空间。

在操作对象时,实际上是在操作对象的引用而不是实际的对象。因此,引用类型的值都是按引用访问的。

这里的引用,我们可以粗浅地理解为保存在栈内存中的一个地址,该地址与堆内存的实际值相关联。

为了更好的搞懂栈内存与堆内存,我们可以结合以下例子与图解进行理解。

```
var a1 = 0;   // 栈 
var a2 = 'this is string'; // 栈
var a3 = null; // 栈

var b = { m: 20 }; // 变量b存在于栈中,{m: 20} 作为对象存在于堆内存中
var c = [1, 2, 3]; // 变量c存在于栈中,[1, 2, 3] 作为对象存在于堆内存中
```
堆栈内存图解.png

上例变量的内存分配情况图解

因此当我们要访问堆内存中的引用数据类型时,实际上我们首先是从栈中获取了该对象的地址引用(或者地址指针),然后再从堆内存中取得我们需要的数据。

三:JavaScript中基础类型和引用类型的特点。

既然已经明白了栈内存和堆内存的存储数据的特点,那么接下来就看一些小的例子,这些小的例子专门用来考察基础类型和引用类型的存储特点

原始类型的复制.png

在栈内存中的数据发生复制行为时,系统会自动为新的变量分配一个新的内存空间。上例中 let b = a 执行之后,a与b虽然值都等于20,但是他们其实已经是相互独立互不影响的值了。具体如图。所以我们修改了b的值以后,a的值并不会发生变化。因此输出的 a 的值还是 20。

引用类型的复制.png

我们通过let n = m 执行一次复制引用类型的操作。引用类型的复制同样也会为新的变量自动分配一个新的值保存在栈内存中,但不同的是,这个新的值,仅仅只是引用类型存在栈内存中的一个地址指针。当地址指针相同时,尽管他们相互独立,但是在堆内存中访问到的具体对象实际上是同一个。如图所示。

因此当我改变n时,m也发生了变化。此时输出的m.a的值也变成了15,这就是引用类型的特性。

如果这样还不好理解,就举一个生活中的例子,假设甲乙两个人一起租房子,那么他们都共同拥有同一个大门进入房间,如果一个人将屋子里面的仅有的空调弄坏了,那么两个人就都没有空调使用了。

四:JavaScript浅克隆和深度克隆

既然已经理解了JS中基础类型和引用类型的特点,下面就开始真正探讨关于深度克隆问题了。

浅克隆之所以被称为浅克隆,是因为对象只会被克隆最外部的一层,至于更深层的对象,则依然是通过引用指向同一块堆内存.

// 浅克隆函数
function shallowClone(o) {
  const obj = {};
  for ( let i in o) {
    obj[i] = o[i];
  }
  return obj;
}
// 被克隆对象
const oldObj = {
  a: 1,
  b: [ 'e', 'f', 'g' ],
  c: { h: { i: 2 } }
};

const newObj = shallowClone(oldObj);
console.log(newObj.c.h, oldObj.c.h); // { i: 2 } { i: 2 }
console.log(oldObj.c.h === newObj.c.h); // true

我们可以很明显地看到,虽然oldObj.c.h被克隆了,但是它还与oldObj.c.h相等,这表明他们依然指向同一段堆内存,我们上面讨论过了引用类型的特点,这就造成了如果对newObj.c.h进行修改,也会影响oldObj.c.h。这本身不是我们想要的,因此就不算是一版好的克隆。

newObj.c.h.i = '我们两个都变了';
console.log(newObj.c.h, oldObj.c.h); // { i: '我们两个都变了' } { i: '我们两个都变了' }

我们改变了newObj.c.h.i的值,oldObj.c.h.i也被改变了,这就是浅克隆的问题所在.

当然,我们这个深克隆还不算完美,例如Buffer对象、Promise、Set、Map可能都需要我们做特殊处理,另外对于确保没有循环引用的对象,我们可以省去对循环引用的特殊处理,因为这很消耗时间,不过一个基本的深克隆函数我们已经实现了。

实现一个完整的深克隆是由许多坑要踩的,npm上一些库的实现也不够完整,在生产环境中最好用lodash的深克隆实现.

参考链接:
https://juejin.im/post/5abb55ee6fb9a028e33b7e0a
https://juejin.im/entry/589c29a9b123db16a3c18adf
https://www.zhihu.com/question/20289071
https://www.zhihu.com/question/47746441?from=profile_question_card
http://laichuanfeng.com/study/javascript-immutable-primitive-values-and-mutable-object-references/

上一篇下一篇

猜你喜欢

热点阅读