本文主要内容转自如何写出一个惊艳面试官的深拷贝?
浅拷贝
创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
深拷贝
将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象
最简版
JSON.parse(JSON.stringify(obj));
复制代码
这种深拷贝方式存在以下问题:
- obj里如果有时间对象,时间对象会被JSON.stringify序列化为字符串
- obj里如果有RegExp、Error对象,将会序列化为空对象
- obj里有函数和undefined,序列化后,函数和undefined会丢失
- obj里有NaN、Infinity和-Infinity,序列化后结果为null
- obj中的对象有构造函数生成的,使用JSON.parse(JSON.stringify(obj))深拷贝后,会丢弃对象的constructor
- obj中存在循环引用的情况也无法正确实现深拷贝
基础版
如果是浅拷贝的话,只需要遍历target对象中所有属性,赋值给目标对象。基本类型直接赋值,引用类型赋值内存地址即可,如下:
function shallowClone(target) {
let clone = {};
for(let i in target) {
clone[i] = target[i];
}
return clone;
}
复制代码
如果是深拷贝的话,通过递归解决,基本类型直接赋值,引用类型通过for…in遍历赋值
function deepClone(target) {
if(typeof target === 'object') {
let clone = {};
for(let key in target) {
clone[key] = deepClone(target[key])
}
return clone;
} else {
return target;
}
}
复制代码
const target = {
a: 1,
b: {
c: true,
d: {
e: 3
}
}
}
复制代码
很显然,此方法可以满足基本深拷贝的需求。但是没有考虑包含数组的情况。
考虑数组的深拷贝
function deepClone(target) {
if (typeof target === 'object') {
let clone = Array.isArray(target) ? [] : {};
for(let key in clone) {
clone[key] = deepClone(target[key]);
}
return clone;
} else {
return target;
}
}
const target = {
field1: 1,
field2: undefined,
field3: {
child: [
{
a: 1
}
]
},
field4: [
{
c: { d: 4 }
},
4,
8
]
};
复制代码
目前实现了对于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 clone = Array.isArray ? [] : {}
if (map.get(target)) {
return map.get(target);
}
map.set(target, clone)
for(let key in target) {
clone[key] = clone(target[key]);
}
return clone;
} else {
return target;
}
}
复制代码
其他数据类型
目前只考虑了普通的object
和array
数据类型,以及基本数据类型。还需要考虑更多数据类型。最准备的类型判断是通过Object.prototype.toString.call来判断。
// getType 获得各种数据类型
function getType(target) {
return Object.prototype.toString.call(target);
}
// isObject 判断target是否为object
function isObject(target) {
const type = typeof target;
return type !== null && (type === 'object' || type === 'function');
}
function deepClone(target, map = new Map()) {
if (!isObject) {
return target;
}
const type = getType(target);
let clone;
// 避免循环引用
if (map.get(target)) {
return map.get(target);
}
map.set(target, clone);
// clone set
if (type === '[object Set]') {
target.forEach(value => {
clone.add(deepClone(value, map));
});
return clone;
}
// clone map
if (type === '[object Map]') {
target.forEach((value, key) => {
clone.set(key, deepClone(value, map))
})
return clone;
}
// clone 对象和数组
if (type === ['object Array'] || type === ['object Object']) {
clone = Array.isArray(target) ? [] : {}
for(let key in target) {
clone[key] = deepClone(target[key], map);
}
}
return clone;
}
复制代码
总结
本文只转载了部分内容,只实现了基本数据类型拷贝和object,array,set,map等引用类型的拷贝,以及循环引用拷贝,更多内容请看原文。