简单的深拷贝:JSON深拷贝
const newObj = JSON.parse(JSON.stringify(obj))
JSON方式实现深拷贝的弊端是:
- 无法拷贝
Symbol和undefined1
。Symbol作为 属性/值 都无法被拷贝到新对象中; 不支持循环引用
。循环引用指的是对象中的一个属性指向了对象本身,原对象若使用了循环引用,那么JSON拷贝的时候就会报错;- 无法拷贝出原对象中的
函数
;
const s1 = Symbol();
const s2 = Symbol();
const obj = {
foo: function() { // 函数的拷贝
console.log("foo function");
},
[s1]: "abc", // Symbol作为key
s2: s2, // Symbol作为value
temp: undefined
}
obj.inner = obj; // 循环引用
const newObj = JSON.parse(JSON.stringify(obj)); // 直接报错
【注:上面说了深拷贝,这里也注明一下常见的浅拷贝】
Object.assign
:Object.assign( { }, obj )Array.prototype.slice()
,Array.prototype.concat()
:数组深拷贝- 使用
扩展运算符
实现的复制:{ ... obj }, [ ... arr ]
手写深拷贝
实现深拷贝1 - 递归基本实现
递归的终止条件
当值不是对象时,停止向下递归;
递归的参数和返回值
参数是要被拷贝的对象值,返回值是拷贝出来的新对象;
单层递归逻辑
首先创建一个新对象,然后遍历传入将被拷贝对象的属性,对该原对象属性执行递归向下遍历,并将遍历创建的结果返回赋给新对象的属性。当for循环遍历结束后,将新对象返回
【注:如何判断是否为对象类型】我们可以自己封装判断对象类型的函数,typeof就可以,但要特判一下null,因为typeof null的结果是true,所以要去掉这一情况:
function isObject(value) {
const valueType = typeof value;
// 注意函数也可以当做对象
return (value !== null) && (valueType === "object" || valueType === "function")
}
代码示例:
function deepClone(originValue) {
// 不是对象就直接返回,这样做的好处是可以保证在向函数传入基本数据类型的时候不出错,直接进行值拷贝
if(!isOjbect(originValue)) return originValue;
// 否则就直接创建一个新的对象
const newObject = {};
for(const key in originValue) { // 遍历对象的key,这样新对象也可以用 [] 来创建新的属性
newObject[key] = deepClone(originValue[key]);
}
return newObject; // 将新对象返回
}
function isObject(value) { // 判断是否为对象
const valueType = typeof value;
return (value !== null) && (valueType === "object" || valueType === "function")
}
实现深拷贝2 - 特殊数据类型的处理
Version 1 的做法中,无论是数组
,还是函数
,还是Set/Map
最后都是被解析成了一个对象(即中括号:{}),且如Symbol作为键会被忽略,Symbol作为值则拷贝两个对象的值可能是一样的,这不符合我们对Symbol的期待等问题,这样的拷贝是合理的,我们应该分情况讨论:
数组
数组和对象类似,同时也可能是要做深拷贝的,所以若类型是数组,我们要创建一个空数组去接收之后深拷贝的内容;
函数
函数本身就是大家共享使用的,所以直接返回,不用特殊处理
Symbol作为值
由于Symbol的值要保持独一无二性,所以我们不能直接拷贝原对象中Symbol的值,而要重新创建,创建时拿到原对象中Symbol的描述再去创建即可:s.description(s为Symbol创建出的)
Symbol作为键
Symbol作为键在for/for-of/for-in直接遍历时遍历不出来的,我们可以通过Object.getOwnPropertySymbols(obj)
拿到所有的Symbol键,在根据这些键按照深拷贝的逻辑再遍历一次
Set/Map
这里也是重新创建,创建过程中通过扩展运算符将原对象中的Set/Map展开后重新创建一遍: [... originSet]
和[... originMap]
【注】所有特殊类型的判断处理逻辑都写在正常逻辑的前面,防止其进入正常逻辑处理环节。
代码示例
// 优化后的深拷贝
function deepClone(originValue) {
// Set/Object通过typeof无法直接判断,只能得出是object的结论,所以要用到instanceof
if(originValue instanceof Set) return new Set([... originValue]);
// 获取set内部的内容,最好的方式就是解构为数组,lc中也有所体现
// map和set是类似(instanceof判断),都是new Map(展开)
if(originValue instanceof Map) return new Map([... originValue]);
// 注意typeof的结果都是小写的
// 判断如果是Symbol的value,那么也创建一个新的Symbol
if(typeof originValue === "symbol") return Symbol(originValue.description);
// 如果是一个函数类型,就直接返回即可(共用),比如java中的类就是这样做的
if(typeof originValue === "function") return originValue;
// 在完成特判后,若发现是基本类型就直接返回
if(!isOjbect(originValue)) return originValue;
// 判断传入的对象是数组,还是对象:
const newObject = Array.isArray(originValue) ? [] : {}; // 是数组就建立数组,否则就是对象
for(const key in originValue) {
newObject[key] = deepClone(originValue[key]);
}
// 上面的for循环是拿不到Symbol作为key的属性的,对Symbol的key进行特殊的处理
const symbolKeys = Object.getOwnPropertySymbols(originValue);
// Object上有这样一个方法可以拿到对象中所有的Symbol作为键的一个数组
for(const sKey of symbolKeys) { // 深拷贝的递归逻辑
newObject[sKey] = deepClone(originValue[sKey]);
}
return newObject;
}
// 判断是否为大范围的对象类型(但我们是反过来用,判断其是否为基本类型)
function isOjbect(value) {
const valueType = typeof value;
return (value !== null) && (valueType === "object" || valueType === "function")
}
实现深拷贝3(终版) - 解决循环引用
上面几个版本没有解决的一个重要问题是:循环引用
,循环引用就是对象中的某个属性指向对象自己,我们在递归遍历对象属性时,若有这一指向对象自身的属性,那么就会一直向下递归没有尽头 。。。
一直遍历下去就会出现死循环,然后就堆栈崩了
在obj创建后,我们希望再次deepClone的时候可直接将其返回,而不是深度递归,具体的实现方案就是:维护一个map,每次创建了一个新的newOjbect后就按照originValue 作为键装入到map中,下一次再遇到时就直接返回结果,不陷入无限递归中
(注意map的键可以是对象的,只要下次再遇到原对象自己<虽然本质上对象还是被转为了字符串,但不同对象的字符串是不同的,这和某个对象作为对象属性是不同的,且WeakMap只能接收对象作为键值>,直接将新对象返回,中止递归)。 即循环引用可以obj.key.key.key...一直引用下去,现在我们在第二时就直接将新创建的对象返回回去。
【对象作为map的键和对象的属性】
某个对象1在对象2中作为属性时,该属性会被直接转为字符串:'[object Object]'
,若在对象中多个对象作属性,那么因为属性值相同,是会发生属性覆盖的:
const info1 = { name: 'info1' };
const info2 = { name: 'info2' };
const obj = {
[info1]: 'obj1',
[info2]: 'obj2'
}
console.log(obj); // { '[object Object]': 'obj2' },只有一个属性
在Map中对象作为属性,虽然也会被转为字符串,但是不同对象所对应的字符串是不同的,所以不同对象可以作为不同的键存在:
const info1 = { name: 'info1' };
const info2 = { name: 'info2' };
const map = new Map([[info1, 'info1'], [info2, 'info2']]);
// Map(2) { { name: 'info1' } => 'info1', { name: 'info2' } => 'info2' }
console.log(map)
map要在哪里创建
若在全局,那么不同对象进行深拷贝的时候都用到map,这样map会不堪重负,所以应该放入到函数当中,但放到函数中每次都会创建一个新的map,这里
提供了一个绝妙的解决思路:就是让map作为函数的一个参数,然后默认创建,每次传递的时候 就将开始创建的map口口相传即可了
。
map在垃圾回收方面的优化
还有map作为参数创建时,构造函数可以用WeakMap(),这样外界在想销毁传入的map时,可以销毁而不是因为强引用的关系而无法销毁
(WeakMap是弱引用,对GC而言,弱引用是不起作用的,只有强引用才能防止被GC垃圾回收掉)。
代码示例
// 其中WeakMap类型的map作为参数传递
function deepClone(originValue, map = new WeakMap()) {
if(originValue instanceof Set) return new Set([... originValue]);
if(originValue instanceof Map) return new Map([... originValue]);
if(typeof originValue === "symbol") return Symbol(originValue.description);
if(typeof originValue === "function") return originValue;
if(!isOjbect(originValue)) return originValue;
// 若发现map中已经存在了,直接将结果返回,中止向下递归
if(map.has(originValue)) return map.get(originValue);
const newObject = Array.isArray(originValue) ? [] : {};
// newObject本身是在最开始的时候obj传入的后就放入到了map当中的(地址),然后后面则不断填充其属性
map.set(originValue, newObject);
for(const key in originValue) {
newObject[key] = deepClone(originValue[key], map); // 传递map
}
const symbolKeys = Object.getOwnPropertySymbols(originValue);
for(const sKey of symbolKeys) {
newObject[sKey] = deepClone(originValue[sKey], map); // 这里的递归同样要传递map
}
return newObject;
}
// 判断是否为对象
function isOjbect(value) {
const valueType = typeof value;
return (value !== null) && (valueType === "object" || valueType === "function")
}
【注:最后的一点优化】我们在进行深拷贝循环的时候还可以先判断一下: originValue.hasOwnProperty(key) 为 true 再拷贝,只拷贝自己的属性,至于原型链上的父类属性等就不做拷贝了!
小结
手写深拷贝总共可以分为3个层次:
- 单层拷贝逻辑的编写:逐层遍历,填充新创建的对象
- 特殊数据类型的处理:数组,函数,Set/Map,Symbol键/值 除函数外,其他均要重新创建
- 循环引用的解决:利用WeakMap中止无限递归,且其放在参数位置较好