你还在用乞丐版的深拷贝么
乞丐版:JSON.parse(JSON.stringify());
这种写法非常简单,而且可以应对大部分的应用场景,但是它还是有很大缺陷的,比如拷贝其他引用类型、拷贝函数、循环引用等情况。
基础版本
如果是浅拷贝的话,下面的很容易:
function clone(target){ let cloneTarget = {}; for (const key in target) { cloneTarget[key] = target[key]; } return cloneTarget; }
创建一个新的对象,遍历需要克隆的对象,将需要克隆对象的属性依次添加到新对象上,返回。
如果是深拷贝的话,考虑到我们要拷贝的对象是不知道有多少层深度的,我们可以用递归来解决问题,稍微改写上面的代码:
- 如果是原始类型,无需继续拷贝,直接返回
- 如果是引用类型,创建一个新的对象,遍历需要克隆的对象,将需要克隆对象的属性执行深拷贝后依次添加到新对象上。
很容易理解,如果有更深层次的对象可以继续递归直到属性为原始类型,这样我们就完成了一个最简单的深拷贝:
function clone (target) { if (typeof target === 'object') { let cloneTarget = {}; for (const key in target) { cloneTarget[key] = clone(target[key]) } return cloneTarget; } else { return target; } }
这是一个最基础版本的深拷贝,这段代码可以让你向面试官展示你可以用递归解决问题,但是显然,他还有非常多的缺陷,比如,还没有考虑数组。
考虑数组
在上面的版本中,我们的初始化结果只考虑了普通的object
,下面我们只需要把初始化代码稍微一变,就可以兼容数组了:
function clone(target) { if (typeof target === 'object') { let cloneTarget = Array.isArray(target) ? [] : {}; for (const key in target) { cloneTarget[key] = clone(target[key]); } return cloneTarget; } else { return target; } };
循环引用
const target = { field1: 1, field2: undefined, field3: { child: 'child' }, field4: [2, 4, 8] }; target.target = target;
解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。
这个存储空间,需要可以存储key-value
形式的数据,且key
可以是一个引用类型,我们可以选择Map
这种数据结构:
- 检查
map
中有无克隆过的对象 - 有 - 直接返回
- 没有 - 将当前对象作为
key
,克隆对象作为value
进行存储 - 继续克隆
function clone(target, map = new Map()) { if (typeof target === 'object') { let cloneTarget = Array.isArray(target) ? [] : {}; if (map.get(target)) { return map.get(target); } map.set(target, cloneTarget); for (const key in target) { cloneTarget[key] = clone(target[key], map); } return cloneTarget; } else { return target; } };
接下来,我们可以使用,WeakMap
提代Map
来使代码达到画龙点睛的作用。
为什么要这样做呢?,先来看看WeakMap
的作用:
WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。
什么是弱引用呢?
在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。
我们默认创建一个对象:const obj = {}
,就默认创建了一个强引用的对象,我们只有手动将obj = null
,它才会被垃圾回收机制进行回收,如果是弱引用对象,垃圾回收机制会自动帮我们回收。
举个例子:
如果我们使用Map
的话,那么对象间是存在强引用关系的:
let obj = { name : 'ConardLi'} const target = new Map(); target.set(obj,'code秘密花园'); obj = null;
虽然我们手动将obj
,进行释放,然是target
依然对obj
存在强引用关系,所以这部分内存依然无法被释放。
再来看WeakMap
:
let obj = { name : 'ConardLi'} const target = new WeakMap(); target.set(obj,'code秘密花园'); obj = null;
如果是WeakMap
的话,target
和obj
存在的就是弱引用关系,当下一次垃圾回收机制执行时,这块内存就会被释放掉。
设想一下,如果我们要拷贝的对象非常庞大时,使用Map
会对内存造成非常大的额外消耗,而且我们需要手动清除Map
的属性才能释放这块内存,而WeakMap
会帮我们巧妙化解这个问题。
https://juejin.cn/post/6844903929705136141#heading-2