# 👉 深浅拷贝的实现
# 直接赋值
当目标对象对赋值对象通过直接赋值的方式进行拷贝,这种情况下,对于原始类型就是直接赋值,而对于引用类型仅能拷贝了地址,改变一方属性值,由于是同一个引用,会导致同个地址对应的属性值都发生变化。
let a = {
age: 1
};
let b = a;
a.age = 2;
console.log(b.age); // 2
# 浅拷贝和深拷贝的区别
# 浅拷贝
浅拷贝会将对象的各个属性进行依次复制,但并不会进行递归复制,即只会赋值目标对象的第一层属性。
对于目标对象第一层为基本数据类型的数据,就是直接赋值,即「传值」;
而对于目标对象第一层为引用数据类型的数据,就是直接赋存于栈内存中的堆内存地址,即「传址」。
常见的浅拷贝:Object.assign
Object.assign 会拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的依然是属性的引用,因此只是浅拷贝,并不是深拷贝。
# 深拷贝
深拷贝不同于浅拷贝,它不只拷贝目标对象的第一层属性,而是递归拷贝目标对象的所有属性。
常用的深拷贝:JSON.parse(JSON.stringify())
JSON.parse(JSON.stringify())存在的问题:
1)对于 Date 类型,会将时间类型序列化成字符串;
2)对于 Error、RegExp 对象只会拷贝得一个空对象;
3)对于 NaN、Infinity、-Infinity,会变成 null;
4)会丢失构造函数的属性方法;
5)无法解决循环引用问题;
# 浅拷贝的简单实现
function shadowClone(target) {
if (typeof target === "object" && target !== null) {
const newTarget = target instanceof Array ? [] : {};
for (let key in target) {
// 避免把继承的属性也一并拷贝
if (target.hasOwnProperty(key)) {
newTarget[key] = target[key];
}
}
return newTarget;
} else {
return target;
}
}
const arr1 = [2, 3, 5, { name: "arr1" }];
const arr2 = shadowClone(arr1);
arr2[3].name = "arr2";
console.log("arr1:", arr1);
console.log("arr2:", arr2);
// arr1:[2, 3, 5, {name: 'arr2'}]
// arr2:[2, 3, 5, {name: 'arr2'}]
# 深拷贝的简单实现
接下来是比较头大的深拷贝,深拷贝要在浅拷贝的基础上进行更深层的递归拷贝, 以及对各种特殊数据类型和边界情况的处理。
这里参考了掘金的一篇文章:如何写出一个惊艳面试官的深拷贝? (opens new window)。
同时,也推荐阅读关于lodash 的深拷贝源码分析 (opens new window)相关的文章,进行更深入理解。
// 判断是否为引用类型
function isObject(source) {
return (
(typeof source === "object" || typeof source === "function") &&
source !== null
);
}
// 获取准确的引用类型
function getType(source) {
return Object.prototype.toString.call(source);
// example:
// Object.prototype.toString.call(true) -> [object Boolean]
// Object.prototype.toString.call(123) -> [object Number]
// Object.prototype.toString.call(null) -> [object Null]
// Object.prototype.toString.call(JSON) -> [object JSON]
// Object.prototype.toString.call(function(){}) -> [object Function]
// Object.prototype.toString.call(new Set()) -> [object Set]
}
// 支持遍历进行深拷贝的引用类型
const supportTraverse = {
"[object Array]": true,
"[object Object]": true,
"[object Map]": true,
"[object Set]": true
};
// 对不支持遍历克隆的引用类型处理
// 多为包装类型对象:Boolean、Number、String、Date、Error、Symbol 直接用构造函数和原始数据创建一个新对象
function handleUnTraverse(target, type) {
const ctor = target.constructor;
switch (type) {
case "[object Boolean]":
return new Object(Boolean.prototype.valueOf.call(target));
case "[object String]":
return new Object(String.prototype.valueOf.call(target));
case "[object Number]":
return new Object(Number.prototype.valueOf.call(target));
case "[object Symbol]":
return new Object(Symbol.prototype.valueOf.call(target));
case "[object Error]":
case "[object Date]":
return new ctor(target);
case "[object Function]":
// 函数需要考虑到的情况比较多也比较麻烦,比如克隆以后可能会涉及函数作用域、上下文变化等问题
// 此处直接返回函数的引用
return target;
default:
return new ctor(target);
}
}
function deepClone(target, map = new WeakMap()) {
// 克隆原始类型数据
if (!isObject(target)) {
return target;
}
// 根据目标对象是否能遍历进行初始化
let targetClone;
const type = getType(target);
if (!supportTraverse[type]) {
// 对不支持递归遍历的对象类型初始化并克隆
return handleUnTraverse(target, type);
} else {
// 新建空对象,保证原型不丢失
const ctor = target.constructor;
targetClone = new ctor();
}
// 解决循环引用
// map 用于标识对象是否已被拷贝过
if (map.get(target)) {
// 已拷贝过的直接返回,否则进行逐个属性的拷贝并标记已拷贝
return target;
}
// 标记已拷贝
map.set(target, true);
// Map 和 Set 的添加元素方法不同,分别为 set 和 add
if (type === "[object Map]") {
target.forEach((value, key) => {
targetClone.set(key, deepClone(value, map));
});
return targetClone;
}
if (type === "[object Set]") {
target.forEach(value => {
targetClone.add(deepClone(value, map));
});
return targetClone;
}
// 克隆对象和数组
for (let propName in target) {
if (target.hasOwnProperty(propName)) {
targetClone[propName] = deepClone(target[propName], map);
}
}
return targetClone;
}
// test
const map = new Map();
map.set("key", "value");
map.set("ConardLi", "code秘密花园");
const set = new Set();
set.add("ConardLi");
set.add("code秘密花园");
const target = {
field1: 1,
field2: undefined,
field3: {
child: "child"
},
field4: [2, 4, 8],
empty: null,
map,
set,
bool: new Boolean(true),
num: new Number(2),
str: new String(2),
symbol: Object(Symbol(1)),
date: new Date(),
// reg: /\d+/,
error: new Error(),
func1: (a, b) => {
return a + b;
},
func2: function() {
console.log("in func2", this, this.field3);
// console.log('code秘密花园');
}
};
const result = deepClone(target);
console.log(target);
target.func2();
console.log(result);
result.func2();
console.log("-----change result field3");
result.field3.child = "result filed3 child";
console.log("target:", target);
target.func2();
console.log("result:", result);
result.func2();