在 JavaScript 开发中,数据的拷贝是一个常见的需求,特别是当你需要复制对象或者数组时。理解浅拷贝和深拷贝的概念,能够帮助我们避免一些常见的错误。
一、浅拷贝
1.1 什么是浅拷贝?
浅拷贝指的是创建一个新的对象或数组,新的对象或数组与原对象或数组在顶层结构上具有相同的值。但如果原对象或数组中包含对象或数组类型的属性(引用类型),则新对象和原对象会共享这些引用类型的值。也就是说,浅拷贝仅复制对象的引用,而不是对象本身。
1.2 浅拷贝的方式
常见的浅拷贝方式有:
(1) Object.assign()
Object.assign() 方法会将源对象的所有可枚举属性浅拷贝到目标对象中。对于嵌套的对象,拷贝的是引用。
let obj = { a: 1, b: { n: 2 } };
let copy = Object.assign({}, obj);
obj.b.n = 200;
console.log(copy.b.n); // 输出 200,因为copy和obj共享相同的b对象
(2) Object.create()
Object.create() 方法通过指定的原型对象来创建一个新对象,新的对象是基于原型的浅拷贝。
let obj = { a: 1, b: { n: 2 } };
let copy = Object.create(obj); //创建一个隐式原型是obj的空对象
copy.a = 10; //{a:10}
obj.b.n = 1
console.log(copy.a); // 输出 10
console.log(copy.b); // 输出 { n: 1 }, 引用共享
(3) 展开运算符 ...
展开运算符也是一种浅拷贝的方式,它可以将一个对象的所有属性展开到新对象中。
let obj = { a: 1, b: { n: 2 } };
let copy = { ...obj };
obj.b.n = 200;
console.log(copy.b.n); // 输出 200
(4) 数组的 slice() 和 concat()
对于数组,slice() 和 concat() 方法可以进行浅拷贝。
let arr = [1, 2, { n: 3 }];
let copy = arr.slice();
arr[2].n = 300;
console.log(copy[2].n); // 输出 300,因为copy和arr共享相同的对象
let arr = [1, 2, 3, { a: 1 }];
let copy = arr.concat(); // 使用concat()方法创建浅拷贝
arr[3].a = 200; // 修改原数组的对象元素
console.log(arr); // 输出 [1, 2, 3, { a: 200 }]
console.log(copy); // 输出 [1, 2, 3, { a: 200 }],因为copy和arr共享引用类型的数据
1.3 浅拷贝的风险
由于浅拷贝是对引用类型的浅层复制,因此在处理复杂对象时,可能会产生意外的副作用。例如,修改拷贝对象中的引用类型数据,也会影响原始对象。
这里还需要注意一下:注意:浅拷贝可能会把人为添加在对象上面的隐式原型也拷贝下来哦!
二、深拷贝
2.1 什么是深拷贝?
深拷贝与浅拷贝不同,它会递归地拷贝对象中的每一层数据,包括所有的嵌套对象或数组。因此,深拷贝得到的新对象与原对象完全独立,新对象的修改不会影响原对象。
2.2 深拷贝的方式
(1) JSON.parse(JSON.stringify(obj))
这是一个最常见的深拷贝实现方式。它通过先将对象转为 JSON 字符串,然后再从字符串中解析出一个新的对象来实现深拷贝。
let obj = { a: 1, b: { n: 2 } };
let copy = JSON.parse(JSON.stringify(obj));
obj.b.n = 200;
console.log(copy.b.n); // 输出 2,深拷贝后copy与obj完全独立
限制:
- 无法复制
undefined、symbol、bigint、时间对象、NAN等特殊数据类型。 - 不能处理函数和正则表达式。
- 会丢失对象的原型链。
(2) 递归拷贝
通过递归遍历对象的每一层,判断数据类型,如果是对象类型,则递归进行拷贝。这是手动实现深拷贝的一种方式。
function deepClone(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
let newObj = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = deepClone(obj[key]);
}
}
return newObj;
}
let obj = { a: 1, b: { n: 2 } };
let copy = deepClone(obj);
obj.b.n = 200;
console.log(copy.b.n); // 输出 2,深拷贝成功
(3) structuredClone()
structuredClone() 是浏览器提供的原生深拷贝方法,能够复制所有可以序列化的 JavaScript 数据类型,支持复制对象中的 Map、Set、Date、ArrayBuffer 等。
let obj = { a: 1, b: { n: 2 } };
let copy = structuredClone(obj);
obj.b.n = 200;
console.log(copy.b.n); // 输出 2,深拷贝成功
注意:此方法的兼容性较差,在一些旧版本的浏览器中可能不支持。
(4) 使用 MessageChannel 实现深拷贝
MessageChannel 是一种较为特殊的方法,通过消息传递的方式实现深拷贝。但我们一般不会用这种方法去深拷贝,有点杀鸡用牛刀。
function deepClone(obj) {
return new Promise((resolve, reject) => {
const { port1, port2 } = new MessageChannel(); //一种消息管道,一般不会用来拷贝
port1.postMessage(obj);
port2.onmessage = (ev) => {
resolve(ev.data);
};
});
}
let obj = { a: 1, b: { n: 2 } };
deepClone(obj).then(copy => {
obj.b.n = 200;
console.log(copy.b.n); // 输出 2,深拷贝成功
});
2.3 深拷贝的优缺点
- 优点:深拷贝可以确保拷贝得到的新对象与原对象完全独立,修改新对象不会影响原对象。
- 缺点:
- 性能开销较大,尤其是对象嵌套层数较深时。
- 需要注意一些特殊数据类型的处理(如
undefined、symbol等)。
三、总结
- 浅拷贝 只会拷贝对象的引用类型,修改拷贝对象中的嵌套数据会影响原对象。
- 深拷贝 会递归地拷贝对象的每一层,避免了浅拷贝中修改嵌套数据的副作用。
选择浅拷贝还是深拷贝,需要根据实际需求来判断。如果只需要复制简单的属性,浅拷贝就足够了;如果需要完全独立的对象副本,深拷贝则更为合适。