深拷贝的三种实现方式分别是:1、递归递归去复制所有层级属性;2、用json对象的parse和stringify实现;3、借用jq的extend方法。
深拷贝的三种实现方式分别是:
1、递归递归去复制所有层级属性
function deepclone(obj){ let objclone = array.isarray(obj)?[]:{}; if(obj && typeof obj===object){ for(key in obj){ if(obj.hasownproperty(key)){ //判断ojb子元素是否为对象,如果是,递归复制 if(obj[key]&&typeof obj[key] ===object){ objclone[key] = deepclone(obj[key]); }else{ //如果不是,简单复制 objclone[key] = obj[key]; } } } } return objclone;} let a=[1,2,3,4], b=deepclone(a);a[0]=2;console.log(a,b);
跟之前想象的一样,现在b脱离了a的控制,不再受a影响了。
这里再次强调,深拷贝,是拷贝对象各个层级的属性,可以看个例子。jq里有一个extend方法也可以拷贝对象,我们来看看
let a=[1,2,3,4], b=a.slice();a[0]=2;console.log(a,b);
那是不是说slice方法也是深拷贝了,毕竟b也没受a的影响,上面说了,深拷贝是会拷贝所有层级的属性,还是这个例子,我们把a改改
let a=[0,1,[2,3],4], b=a.slice();a[0]=1;a[2][0]=1;console.log(a,b);
拷贝的不彻底啊,b对象的一级属性确实不受影响了,但是二级属性还是没能拷贝成功,仍然脱离不了a的控制,说明slice根本不是真正的深拷贝。
这里引用知乎问答里面的一张图
第一层的属性确实深拷贝,拥有了独立的内存,但更深的属性却仍然公用了地址,所以才会造成上面的问题。
同理,concat方法与slice也存在这样的情况,他们都不是真正的深拷贝,这里需要注意。
2.除了递归,我们还可以借用json对象的parse和stringify
function deepclone(obj){ let _obj = json.stringify(obj), objclone = json.parse(_obj); return objclone} let a=[0,1,[2,3],4], b=deepclone(a);a[0]=1;a[2][0]=1;console.log(a,b);
可以看到,这下b是完全不受a的影响了。
附带说下,json.stringify与json.parse除了实现深拷贝,还能结合localstorage实现对象数组存储。有兴趣可以阅读博主这篇文章。
localstorage存储数组,对象,localstorage,sessionstorage存储数组对象
3.除了上面两种方法之外,我们还可以借用jq的extend方法。
$.extend( [deep ], target, object1 [, objectn ] )
deep表示是否深拷贝,为true为深拷贝,为false,则为浅拷贝
target object类型 目标对象,其他对象的成员属性将被附加到该对象上。
object1 objectn可选。 object类型 第一个以及第n个被合并的对象。
let a=[0,1,[2,3],4], b=$.extend(true,[],a);a[0]=1;a[2][0]=1;console.log(a,b);
可以看到,效果与上面方法一样,只是需要依赖jq库。
说了这么多,了解深拷贝也不仅仅是为了应付面试题,在实际开发中也是非常有用的。例如后台返回了一堆数据,你需要对这堆数据做操作,但多人开发情况下,你是没办法明确这堆数据是否有其它功能也需要使用,直接修改可能会造成隐性问题,深拷贝能帮你更安全安心的去操作数据,根据实际情况来使用深拷贝,大概就是这个意思。
4.lodash的_.clonedeep()
以下是我参看的一位关于深拷贝的问题解决。
json.parse先将一个对象转为json对象。然后再解析这个json对象。
let obj = {a:{b:22}};let copy = json.parse(json.stringify(obj));
这种方法的优点就是代码写起来比较简单。但是缺点也是显而易见的。你先是创建一个临时的,可能很大的字符串,只是为了把它重新放回解析器。另一个缺点是这种方法不能处理循环对象。
如下面的循环对象用这种方法的时候会抛出异常
let a = {};let b = {a};a.b = b;let copy = json.parse(json.stringify(a));
诸如 map, set, regexp, date, arraybuffer 和其他内置类型在进行序列化时会丢失。
let a = {};let b = new set();b.add(11);a.test = b;let copy = json.parse(json.stringify(a));
a 的值打印如下
copy的值打印如下
对比发现,set已丢失。
structured clone 结构化克隆算法messagechannel
建立两个端,一个端发送消息,另一个端接收消息。
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 api
利用history.replacestate。这个api在做单页面应用的路由时可以做无刷新的改变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 = bvar copy = structuralclone(obj); console.log(copy);
这个方法的优点是。能解决循环对象的问题,也支持许多内置类型的克隆。并且是同步的。但是缺点就是有的浏览器对调用频率有限制。比如safari 30 秒内只允许调用 100 次
notification api
这个api主要是用于桌面通知的。如果你使用facebook的时候,你肯定会发现时常在浏览器的右下角有一个弹窗,对就是这家伙。我们也可以利用这个api实现js对象的深拷贝。
function structuralclone(obj) { return new notification('', {data: obj, silent: true}).data;}var obj = {};var b = {obj};obj.b = bvar copy = structuralclone(obj);console.log(copy)
同样是优点和缺点并存,优点就是可以解决循环对象问题,也支持许多内置类型的克隆,并且是同步的。缺点就是这个需要api的使用需要向用户请求权限,但是用在这里克隆数据的时候,不经用户授权也可以使用。在http协议的情况下会提示你再https的场景下使用。
lodash的_.clonedeep()
支持循环对象,和大量的内置类型,对很多细节都处理的比较不错。推荐使用。
支持的类型有很多
我们这里再次关注一下lodash是如何解决循环应用这个问题的?
从相关的代码中。我们可以发现。lodash是用一个栈记录了。所有被拷贝的引用值。如果再次碰到同样的引用值的时候,不会再去拷贝一遍。而是利用之前已经拷贝好的值。
实现一个简易点的深拷贝,以解决循环引用的问题为目标我们仅仅实现一个简易点的深拷贝。能优雅的处理循环引用的即可。在实现深拷贝之前,我们首先温习回顾一下js中的遍历对象的属性的方法和各种方法的优缺点。
js中遍历一个对象的属性的方法
object.keys() 仅仅返回自身的可枚举属性,不包括继承来的,更不包括symbol属性object.getownpropertynames() 返回自身的可枚举和不可枚举属性。但是不包括symbol属性object.getownpropertysymbols() 返回自身的symol属性for...in 可以遍历对象的自身的和继承的可枚举属性,不包含symbol属性reflect.ownkeys() 返回对象自身的所有属性,不管是否可枚举,也不管是否是symbol。注意不包括继承的属性实现深拷贝,解决循环引用问题
/** * 判断是否是基本数据类型 * @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 = bvar copy = clonedeep(obj); console.log(copy);
以上就是深拷贝的三种实现方式是什么的详细内容。