在JS中,深拷贝和浅拷贝是针对对象(Object)和数组(Array)这类复杂数据类型复制时的概念。
浅拷贝: 当进行浅拷贝时,只是将对象或数组的引用复制一份给新的变量。这意味着新旧变量指向的是同一个内存空间中的数据结构,修改其中一个变量会影响到另一个变量,因为它们实际上是共享同一份数据。
深拷贝: 则是创建一个与原对象完全独立的对象副本,对于原对象内部所包含的所有层级的数据(包括嵌套的对象或数组),都会递归地进行复制,从而保证了新旧对象间不存在任何共享的引用。
简单来说,对拷贝后的数组或对象进行修改,会影响原数组或对象的就是浅拷贝,不会影响的就是深拷贝。
Object.assign()
基本用法:将一个或多个源对象的属性复制到目标对象,并返回目标对象。这一过程是浅拷贝的,即对于嵌套对象或数组,只是拷贝了引用而非创建新的对象。
示例:
const obj = {
a: 1,
b: {
c: 2
},
d: ["e"]
};
const cloneObj = Object.assign({}, obj);
cloneObj.b.c = 3; // 修改拷贝后的对象
console.log("obj: ", obj);
console.log("cloneObj: ", cloneObj);
结果:
从结果可以看到,修改 clonedObj 的属性也会影响到原始对象 obj,所以Object.assign()
实现的是浅拷贝。
在ES6中,...
运算符被称为扩展(Spread)运算符,可以用于数组和对象的浅拷贝。
示例1:拷贝对象
const obj2 = {
a: 1,
b: {
c: 2
},
d: ["hello"]
};
const cloneObj2 = { ...obj2 };
cloneObj2.b.c = 2222222;
console.log("obj2: ", obj2);
console.log("cloneObj2: ", cloneObj2);
示例2:拷贝数组
const arr = ["1", { a: 2 }, 3, true, undefined];
const arrClone = [...arr];
arrClone[0] = "first"; // 修改数组第1个元素
arrClone[1].a = "hello"; // 修改数组第2个元素
console.log("arr: ", arr);
console.log("arrClone: ", arrClone);
从...
拷贝数组结果来看,原始数组和拷贝数组是独立的,但由于数组中包含的对象引用并未改变,所以修改拷贝数组中对象的属性时,原始数组中对应的对象也会受到影响。
也就是说,对于简单数组,...
可以实现数组深拷贝,但是复杂数组…则不能用做实现深拷贝的方法。
利用 JSON 的序列化和反序列化可以实现对象的深拷贝。原理是:JSON.stringify()
会递归遍历对象的所有属性(包括嵌套的对象和数组),将其转换为JSON字符串;然后JSON.parse()
则会根据JSON字符串创建新的JavaScript对象。
示例:
const obj1 = {
a: 1,
b: {
c: 2
},
d: ["hello"]
};
const cloneObj1 = JSON.parse(JSON.stringify(obj1));
cloneObj1.b.c = 3;
console.log("obj1: ", obj);
console.log("cloneObj1: ", cloneObj);
结果:
从结果看到,修改拷贝后的对象其中的属性值,并不会改变原对象的属性值,所以实现的是一个深拷贝。
!!!注意这种方法有局限:
function deepClone1(obj) {
if (typeof obj !== "object") return;
let newObj = obj instanceof Array ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] =
typeof obj[key] === "object" ? deepClone1(obj[key]) : obj[key];
}
}
return newObj;
}
// 测试
const obj5 = {
a: 1,
b: {
c: 2
},
d: ["hello"],
e: new Date(),
f: new Error("error"),
g: new RegExp("/^(.*\..{4}).*$/")
};
const cloneObj5 = deepClone1(obj5);
cloneObj5.b.c = "hhhhhh";
console.log("obj5: ", obj5);
console.log("cloneObj5: ", cloneObj5);
// 判断是否为object类型
function isObject(target) {
return (
(typeof target === "object" && target) || typeof target === "function"
);
}
function deepClone(data, map = new WeakMap()) {
// 基础类型直接返回值
if (!isObject(data)) {
return data;
}
// 日期或者正则对象则直接构造一个新的对象返回
if ([Date, RegExp].includes(data.constructor)) {
return new data.constructor(data);
}
// 处理函数对象
if (typeof data === "function") {
return new Function("return " + data.toString())();
}
// 如果该对象已存在,则直接返回该对象
const exist = map.get(data);
if (exist) {
return exist;
}
// 处理Map对象
if (data instanceof Map) {
const result = new Map();
map.set(data, result);
data.forEach((val, key) => {
// 注意:map中的值为object的话也得深拷贝
if (isObject(val)) {
result.set(key, deepClone(val));
} else {
result.set(key, val);
}
});
return result;
}
// 处理Set对象
if (data instanceof Set) {
const result = new Set();
map.set(data, result);
data.forEach(val => {
if (isObject(val)) {
// 注意:set中的值为object的话也得深拷贝
result.add(deepClone(val));
} else {
result.add(val);
}
});
return result;
}
// 收集键名(考虑了以Symbol作为key以及不可枚举的属性)
const keys = Reflect.ownKeys(data);
// 利用 Object 的 getOwnPropertyDescriptors 方法可以获得对象的所有属性以及对应的属性描述
const allDesc = Object.getOwnPropertyDescriptors(data);
// 结合 Object 的 create 方法创建一个新对象,并继承传入原对象的原型链, 这里得到的result是对data的浅拷贝
const result = Object.create(Object.getPrototypeOf(data), allDesc);
// 新对象加入到map中,进行记录
map.set(data, result);
// Object.create()是浅拷贝,所以要判断并递归执行深拷贝
keys.forEach(key => {
const val = data[key];
if (isObject(val)) {
// 属性值为 对象类型 或 函数对象 的话也需要进行深拷贝
result[key] = deepClone(val);
} else {
result[key] = val;
}
});
return result;
}
// 测试
const obj4 = {
a: 1,
b: {
c: 2
},
d: ["hello"],
e: new Date(),
f: new Error("error"),
g: new RegExp("/^(.*\..{4}).*$/")
};
const cloneObj4 = deepClone(obj4);
cloneObj4.b.c = "hhhhhh";
console.log("obj4: ", obj4);
console.log("cloneObj4: ", cloneObj4);
例如:import { cloneDeep } from 'lodash';
这种拿来就可以用的方法就不做过多介绍了~
总的来说,js中的深、浅拷贝其实就是一个基本功,在实际业务场景中,可根据数据复杂程度自行选择各种实现方式,加油⛽️