在 JavaScript 的编程世界里,数据的复制操作就像一场精心设计的 “数据分身术”。当我们希望创建一个与原对象外观相同的副本时,拷贝操作便登场了。不过,根据对数据处理的深度不同,拷贝又分为浅拷贝和深拷贝,它们各自有着独特的实现方法与应用场景。
一、拷贝的基本概念:复刻对象的 “分身术”
拷贝,简单来说就是复刻一个对象,使其与原对象看起来一模一样。在 JavaScript 中,基于数据类型的不同,拷贝的表现和实现方式也有所差异。基本数据类型(如数字、字符串、布尔值等)的拷贝较为直接,新变量会拥有独立于原变量的值;而引用数据类型(对象、数组等)的拷贝则相对复杂,涉及到内存地址的处理,这也正是浅拷贝和深拷贝概念产生的根源。

二、浅拷贝:表层的 “镜像复制”
浅拷贝,就如同给对象拍摄一张 “表层快照”,它仅仅复制对象最外层的属性。对于引用数据类型的属性,浅拷贝不会深入复制其内部结构,而是让新对象和原对象共享这些属性的内存地址。这就意味着,如果原对象中引用数据类型的属性发生改变,新对象中对应的属性也会随之改变。
浅拷贝的实现方法
1. Object.create(obj)
通过该方法创建的新对象,其原型会指向传入的obj对象,同时会将obj对象自身的可枚举属性复制到新对象上。这是一种基于原型链的浅拷贝方式,例如:
const original = { a: 1, b: { c: 2 } };
const copy = Object.create(original);
original.b.c = 3;
console.log(copy.b.c); // 输出 3
2. [].concat(arr)
对于数组,concat方法可以将传入的数组连接到原数组,并返回一个新数组,实现浅拷贝。它会将原数组的元素复制到新数组中,但对于数组中的对象元素,只是复制引用
const arr1 = [1, { x: 10 }];
const arr2 = [].concat(arr1);
arr1[1].x = 20;
console.log(arr2[1].x); // 输出 20
3. 数组解构[...arr]
使用展开运算符进行数组解构,也能快速实现浅拷贝。新数组会拥有与原数组相同的元素,但同样对于对象元素仅复制引用
const arr3 = [2, { y: 30 }];
const arr4 = [...arr3];
arr3[1].y = 40;
console.log(arr4[1].y); // 输出 40
4. arr.slice(0, arr.length)
slice方法可以截取数组的一部分或全部元素,当传入0和数组长度时,会返回一个包含原数组所有元素的新数组,实现浅拷贝:
const arr5 = [3, { z: 50 }];
const arr6 = arr5.slice(0, arr5.length);
arr5[1].z = 60;
console.log(arr6[1].z); // 输出 60
5. Object.assign({}, obj)
该方法可以将一个或多个源对象的属性复制到目标对象中,常用于对象的浅拷贝。它会将源对象的属性逐一复制到新创建的空对象上,但对于对象属性同样是浅复制
const obj1 = { p: 1, q: { r: 2 } };
const obj2 = Object.assign({}, obj1);
obj1.q.r = 3;
console.log(obj2.q.r); // 输出 3
6. arr.toReversed().reverse()
toReversed方法会返回一个新的反转后的数组,再通过reverse方法还原顺序,也能实现数组的浅拷贝,同样存在对对象元素浅复制的情况
const arr7 = [4, { w: 70 }];
const arr8 = arr7.toReversed().reverse();
arr7[1].w = 80;
console.log(arr8[1].w); // 输出 80
三、深拷贝:深层的 “独立复刻”
深拷贝追求的是创建一个与原对象完全独立的副本,它会递归地遍历对象的每一层属性,对于引用数据类型的属性,也会深入复制其内部结构,确保新对象和原对象在内存中完全分离。这样一来,无论原对象如何变化,新对象都不会受到影响。
深拷贝的实现方法
1. JSON.parse(JSON.stringify(obj))
这是一种较为简单的深拷贝方式,通过将对象转换为 JSON 字符串,再解析回对象来实现。但它存在明显的局限性,无法识别bigint类型,无法处理undefined、symbol、function,并且也无法处理循环引用的情况。例如:
const originalObj = {
num: 1n,
func: () => {},
sym: Symbol('test'),
undef: undefined
};
const copiedObj = JSON.parse(JSON.stringify(originalObj));
console.log(copiedObj.num); // 输出 null
console.log(copiedObj.func); // 输出 undefined
console.log(copiedObj.sym); // 输出 undefined
console.log(copiedObj.undef); // 输出 undefined
2. structuredClone(obj)
这是现代 JavaScript 提供的一个专门用于深拷贝的方法,它能够处理更多复杂的数据类型,包括function、undefined等,并且可以处理循环引用。不过,它在一些旧版本的浏览器中可能不被支持
const complexObj = { a: 1, b: [2, 3], func: () => {} };
const clonedObj = structuredClone(complexObj);
complexObj.b[0] = 4;
console.log(clonedObj.b[0]); // 输出 2
四. 手写深浅拷贝
对对象这个数据类型实现
浅拷贝
只拷贝一层
function shallowCopy(obj){
let newObj = {}
for(let key in obj){
if(obj.hasOwnProperty(key)){
newObj[key]= obj[key]
}
}
return newObj
}
深拷贝
递归拷贝每一层
function deepCopy(obj){
let newObj = {}
for(let key in obj){
if(obj.hasOwnProperty(key)){
if(typeof obj[key] === 'object'&& obj[key]!==null){
newObj[key] = deepCopy(obj[key])//递归拷贝引用类型的每一层
}else{
newObj[key] = obj[key]
}
}
}
return newObj;
}
五、深浅拷贝的应用场景
在实际开发中,选择浅拷贝还是深拷贝,需要根据具体的业务需求来决定。浅拷贝由于操作简单、性能较高,适用于对数据结构要求不高,或者数据结构较为简单,不存在深层嵌套引用数据类型的场景,比如快速复制一个简单的配置对象。而深拷贝则适用于需要完全隔离数据,防止数据之间相互干扰的场景,例如在处理复杂的状态管理、数据缓存,或者进行数据的备份和恢复时。
总之,深入理解 JavaScript 的深浅拷贝,掌握它们的实现方法与应用场景,能够帮助我们在编程过程中更加灵活、准确地处理数据,编写出更健壮、高效的代码。无论是面对简单的数据复制,还是复杂的数据结构处理,都能游刃有余地选择合适的拷贝方式,让程序运行得更加稳定可靠。