js深拷贝是一件看起来很简单的事情,但其实一点儿也不简单。对于循环引用的问题还有一些内置数据类型的拷贝,如Map, Set, RegExp, Date, ArrayBuffer 和其他内置类型。处理起来并非像想象的那么简单。下面终结一下在实际的项目中,使用的一些深拷贝的方法的优缺点。大家也可以关注我的GitHub,互相交流学习进步。
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 = b
var 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 = b
var copy = structuralClone(obj);
console.log(copy)
同样是优点和缺点并存,优点就是可以解决循环对象问题,也支持许多内置类型的克隆,并且是同步的。缺点就是这个需要api的使用需要向用户请求权限,但是用在这里克隆数据的时候,不经用户授权也可以使用。在http协议的情况下会提示你再https的场景下使用。
lodash的_.cloneDeep()
支持循环对象,和大量的内置类型,对很多细节都处理的比较不错。推荐使用。
支持的类型有很多
我们这里再次关注一下lodash是如何解决循环应用这个问题的?
从相关的代码中。我们可以发现。lodash是用一个栈记录了。所有被拷贝的引用值。如果再次碰到同样的引用值的时候,不会再去拷贝一遍。而是利用之前已经拷贝好的值。
lodash深拷贝的详细的源码可以在这里查看。
https://github.com/lodash/lod...
实现一个简易点的深拷贝,以解决循环引用的问题为目标
我们仅仅实现一个简易点的深拷贝。能优雅的处理循环引用的即可。在实现深拷贝之前,我们首先温习回顾一下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 = b
var copy = cloneDeep(obj);
console.log(copy);
完美。大功告成
我们虽然的确解决了深拷贝的大部分问题。不过很多细节还没有去处理。在生产环境,我们还是要使用lodash的cloneDeep。cloneDeep对每个数据类型都单独处理的非常好。比如ArrayBuffer什么的。我们都没有处理。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。