手写深拷贝
深拷贝与浅拷贝
- 浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是引用类型,拷贝的就是内存地址,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
- 深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。
赋值/深拷贝/浅拷贝的区别
- 赋值:赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。
- 浅拷贝:重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响。
- 深拷贝:从堆内存中开辟一个新的区域存放新对象,对对象中的子对象进行递归拷贝,拷贝前后的两个对象互不影响。
总的来说:赋值不是拷贝,相当于把原对象直接换个名字直接用,一改全改;浅拷贝是创建新对象,但仅把第一层赋值过来,因此不会影响基本数据类型,会影响引用类型;深拷贝是浅拷贝的加强版,会通过递归把引用类型更深层的值都赋值过来。
浅拷贝的实现方式
Array.prototype.sliceArray.prototype.concatObject.assign- 拓展运算符
深拷贝的实现方式
JSON.parse(JSON.stringify())--- 这种方法虽然可以实现数组或对象深拷贝,但不能处理函数和正则。
代码实现
面试够用版:
function cloneDeep(obj, map = new WeakMap()) {
if (!isObject(obj)) return obj;
if (map.has(obj)) return map.get(obj);
let target = new obj.constructor();
map.set(obj, target);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
target[key] = cloneDeep(obj[key], map);
}
}
return target;
}
完整版:
const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
const argsTag = '[object Arguments]';
const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';
// 可继续遍历类型
const deepTag = [mapTag, setTag, arrayTag, objectTag, argsTag];
function isObject(target) {
const type = typeof target;
return target !== null && (type === 'object' || type === 'function');
}
function getType(target) {
return Object.prototype.toString.call(target);
}
function getInit(target) {
const Ctor = target.constructor;
return new Ctor();
}
function cloneSymbol(target) {
return Object(Symbol.prototype.valueOf.call(target));
}
function cloneRegExp(target) {
const reFlags = /\w*$/;
const result = new target.constructor(target.source, reFlags.exec(target));
return result;
}
function cloneFunction() {}
function cloneOtherType(target, type) {
const Ctor = target.constructor;
switch (type) {
case boolTag:
case numberTag:
case stringTag:
case errorTag:
case dateTag:
return new Ctor(target.valueOf());
case regexpTag:
return cloneRegExp(target);
case symbolTag:
return cloneSymbol(target);
case funcTag:
return cloneFunction(target);
default:
return null;
}
}
/**
* 深拷贝
* @param {any} target
*/
function cloneDeep(target, map = new WeakMap()) {
if (!isObject(target)) {
return target;
}
const type = getType(target);
let cloneTarget;
if (deepTag.includes(type)) {
cloneTarget = getInit(target, type);
} else {
return cloneOtherType(target, type);
}
if (map.has(target)) {
return map.get(target);
}
map.set(target, cloneTarget);
if (type === setTag) {
target.forEach((value) => {
cloneTarget.add(cloneDeep(value, map));
});
return cloneTarget;
}
if (type === mapTag) {
target.forEach((value, key) => {
cloneTarget.set(key, cloneDeep(value, map));
});
return cloneTarget;
}
for (let key in target) {
if (target.hasOwnProperty(key)) {
cloneTarget[key] = cloneDeep(target[key], map);
}
}
return cloneTarget;
}
总结
手写深拷贝需要考虑很多的情况,如:数据类型判断,基本数据类型、引用数据类型,可遍历类型、不可遍历类型,是否使用对象原型进行重新构建,递归调用等等,函数的复制尤为坑。若业务代码中存在复杂场景的数据深拷贝,还是建议使用 lodash 等完善的库,一是省时省力不重复造轮子,二是这些库经过多年的发展与维护,已经形成了较为完善的逻辑,足以应对大多数使用场景。
参考: