js深拷贝是一件看起来很简单的事情,但其实一点儿也不简单。对于循环引用的问题还有一些内置数据类型的拷贝,如Map, Set, RegExp, Date, ArrayBuffer 和其余内置类型。处理起来并不是像想象的那么简单。下面终结一下在实际的项目中,使用的一些深拷贝的方法的优缺点。你们也能够 关注个人GitHub,互相交流学习进步。
先将一个对象转为json对象。而后再解析这个json对象。git
let obj = {a:{b:22}}; let copy = JSON.parse(JSON.stringify(obj));
这种方法的优势就是代码写起来比较简单。可是缺点也是显而易见的。你先是建立一个临时的,可能很大的字符串,只是为了把它从新放回解析器。另外一个缺点是这种方法不能处理循环对象。github
以下面的循环对象用这种方法的时候会抛出异常算法
let a = {}; let b = {a}; a.b = b; let copy = JSON.parse(JSON.stringify(a));
诸如 Map, Set, RegExp, Date, ArrayBuffer 和其余内置类型在进行序列化时会丢失。json
let a = {}; let b = new Set(); b.add(11); a.test = b; let copy = JSON.parse(JSON.stringify(a));
a 的值打印以下api
copy的值打印以下浏览器
对比发现,Set已丢失。异步
创建两个端,一个端发送消息,另外一个端接收消息。post
function structuralClone(obj) { return new Promise(resolve =>{ const {port1, port2} = new MessageChannel(); port2.onmessage = ev => resolve(ev.data); port1.postMessage(obj); }) } const obj = /* ... */; structuralClone(obj).then(res=>{ console.log(res); })
这种方法的优势就是能解决循环引用的问题,还支持大量的内置数据类型。缺点就是这个方法是异步的。学习
利用history.replaceState。这个api在作单页面应用的路由时能够作无刷新的改变url。这个对象使用结构化克隆,并且是同步的。可是咱们须要注意,在单页面中不要把原有的路由逻辑搞乱了。因此咱们在克隆完一个对象的时候,要恢复路由的原状。url
function structuralClone(obj) { const oldState = history.state; history.replaceState(obj, document.title); const copy = history.state; history.replaceState(oldState, document.title); return copy; } var obj = {}; var b = {obj}; obj.b = b var copy = structuralClone(obj); console.log(copy);
这个方法的优势是。能解决循环对象的问题,也支持许多内置类型的克隆。而且是同步的。可是缺点就是有的浏览器对调用频率有限制。好比Safari 30 秒内只容许调用 100 次
这个api主要是用于桌面通知的。若是你使用Facebook的时候,你确定会发现时常在浏览器的右下角有一个弹窗,对就是这家伙。咱们也能够利用这个api实现js对象的深拷贝。
function structuralClone(obj) { return new Notification('', {data: obj, silent: true}).data; } var obj = {}; var b = {obj}; obj.b = b var copy = structuralClone(obj); console.log(copy)
一样是优势和缺点并存,优势就是能够解决循环对象问题,也支持许多内置类型的克隆,而且是同步的。缺点就是这个须要api的使用须要向用户请求权限,可是用在这里克隆数据的时候,不经用户受权也可使用。在http协议的状况下会提示你再https的场景下使用。
支持循环对象,和大量的内置类型,对不少细节都处理的比较不错。推荐使用。
支持的类型有不少
咱们这里再次关注一下lodash是如何解决循环应用这个问题的?
从相关的代码中。咱们能够发现。lodash是用一个栈记录了。全部被拷贝的引用值。若是再次碰到一样的引用值的时候,不会再去拷贝一遍。而是利用以前已经拷贝好的值。
lodash深拷贝的详细的源码能够在这里查看。
https://github.com/lodash/lod...
咱们仅仅实现一个简易点的深拷贝。能优雅的处理循环引用的便可。在实现深拷贝以前,咱们首先温习回顾一下js中的遍历对象的属性的方法和各类方法的优缺点。
/** * 判断是不是基本数据类型 * @param value */ function isPrimitive(value){ return (typeof value === 'string' || typeof value === 'number' || typeof value === 'symbol' || typeof value === 'boolean') } /** * 判断是不是一个js对象 * @param value */ function isObject(value){ return Object.prototype.toString.call(value) === "[object Object]" } /** * 深拷贝一个值 * @param value */ function cloneDeep(value){ // 记录被拷贝的值,避免循环引用的出现 let memo = {}; function baseClone(value){ let res; // 若是是基本数据类型,则直接返回 if(isPrimitive(value)){ return value; // 若是是引用数据类型,咱们浅拷贝一个新值来代替原来的值 }else if(Array.isArray(value)){ res = [...value]; }else if(isObject(value)){ res = {...value}; } // 检测咱们浅拷贝的这个对象的属性值有没有是引用数据类型。若是是,则递归拷贝 Reflect.ownKeys(res).forEach(key=>{ if(typeof res[key] === "object" && res[key]!== null){ //此处咱们用memo来记录已经被拷贝过的引用地址。以此来解决循环引用的问题 if(memo[res[key]]){ res[key] = memo[res[key]]; }else{ memo[res[key]] = res[key]; res[key] = baseClone(res[key]) } } }) return res; } return baseClone(value) }
验证咱们写的cloneDeep是否能解决循环应用的问题
var obj = {}; var b = {obj}; obj.b = b var copy = cloneDeep(obj); console.log(copy);
完美。大功告成
咱们虽然的确解决了深拷贝的大部分问题。不过不少细节尚未去处理。在生产环境,咱们仍是要使用lodash的cloneDeep。cloneDeep对每一个数据类型都单独处理的很是好。好比ArrayBuffer什么的。咱们都没有处理。