引言
在JavaScript编程中,处理对象和数组时,拷贝操作是一个常见的需求。无论是为了防止意外修改原始数据,还是为了创建数据的独立副本,了解并掌握浅拷贝和深拷贝的概念及实现方法,对我们在处理问题时都非常有用。那么呆同学今天就来详细探讨浅拷贝和深拷贝的区别、常见实现方法及其在实际开发中的应用场景,帮助你在编程中更有效地管理和操作数据。
何为拷贝?
在JavaScript中,拷贝通常针对引用类型。引用类型包括对象、数组、函数等,而基本类型(如数字、字符串、布尔值)在赋值和传递时会直接复制值本身,这是因为原始类型在存储上便与引用类型有所不同。
原始类型存储
对于原始类型值的存储,js 的 v8 引擎会将它们直接存储在 栈 中。栈是一种线性结构,采用后进先出(LIFO)的方式存储数据。原始类型的一个关键特性是它们的赋值和传递是通过值复制的。这意味着,当你将一个原始类型的变量赋值给另一个变量时,会创建这个值的一个独立副本,也就是另外分配新的内存空间,两个变量之间没有任何关联。例如:
let x = 10;
let y = x;
y = 20;
console.log(x); // 输出 10
x 和 y 是两个独立的变量,y 的修改不会影响 x。
引用类型存储
对于引用类型值的存储,js 的 v8 引擎会在 堆 中为其分配一段内存空间,并将其地址存储在栈中。堆是一种非线性结构,以链表的形式存储数据,每个节点包含了对象的属性和方法等信息。与原始类型不同,引用类型的一个重要特性是它们通过引用传递,而不是复制实际值。这意味着当你将一个对象赋值给另一个变量时,两个变量引用的是同一个内存地址。如果你修改其中一个变量的属性,另一个变量也会反映出这个变化。例如:
let obj1 = { a: 1 };
let obj2 = obj1;
obj2.a = 2;
console.log(obj1.a); // 输出 2
obj1 和 obj2 引用的是同一个对象,因此修改 obj2 的属性会影响 obj1。这是因为它们在内存中以引用的方式存储,即变量存储的是对象的地址,而非对象本身。
而究竟拷贝的是原对象的引用,还是基于原对象创建一个完全独立的新对象,将原对象以及其所有的内容完全拷贝,这也是我们区分浅拷贝和深拷贝的重要依据。
那么在知道了拷贝以及类型存储的方式之后,接下来让我们一起来深入学习浅拷贝与深拷贝吧。
浅拷贝
浅拷贝是基于原对象创建一个新对象,拷贝过程中仅复制原对象的引用,而不是其实际内容。这意味着,如果原对象中的内容发生变化,新对象也会受到影响。
换句话说,浅拷贝创建了一个新对象,这个对象有着与原对象相同的属性值,但是这些属性值中的引用类型仍然指向相同的内存地址。
常用的浅拷贝方法
Object.create(x):通过创建一个新空对象,并将其原型设置为指定的对象x,并将其原型上有的属性copy到新对象中来进行浅拷贝。
let a = {
name: '张三',
};
let b = Object.create(a); // 创建一个新的空对象{}
console.log(b); // 输出{}
a.name = '李四';
console.log(b.name); // 输出李四
Object.assign({}, a):创建了一个空对象,将一个或多个源对象的属性copy到目标对象中。
let a = {
name: '张三',
like: {
n: '吃'
}
}
let c = Object.assign({}, a)
console.log(c); // 输出{ name: '张三', like: { n: '吃' } }
[].concat(arr):用于合并数组,这里创建了一个空数组[],并将原数组的元素添加到这个空数组中,从而实现浅拷贝。
const arr = [1, 2, 3];
const newArr = [].concat(arr);
console.log(newArry); // 输出 [1, 2, 3]
- 数组解构
...a:数组解构语法...创建了一个新数组,并将原数组的所有元素复制到新数组中。
const arr = [1, 2, 3];
const newArr = [...arr];
console.log(newArr); // 输出 [1, 2, 3]
arr.slice(0):返回一个新的数组对象,这一新数组是一个从原数组中提取的浅拷贝,这里slice(0)提取了整个原数组。
const arr = [1, 2, 3];
const newArr = original.slice(0);
console.log(newArr); // 输出 [1, 2, 3]
arr.toReversed().reverse():toReversed创建数组的反转副本,reverse再次反转该副本以恢复原顺序,生成与原数组相同的新数组。
const arr = [1, 2, 3];
const newArr = arr.toReversed().reverse();
console.log(newArr); // 输出 [1, 2, 3]
手写一个浅拷贝方法
浅拷贝的实现通常通过遍历原对象的属性,将这些属性及其值复制到新对象中。这种方法可以确保第一层的属性被独立复制,但是对于嵌套的对象或数组,复制的仍然是引用。
实现原理
-
借助for in 遍历原对象,将原对象的属性值增加在新对象中。
-
因为 for in 会遍历到对象隐式具有的属性,通常要使用 obj.hasOwnProperty(key) 来判断拷贝的属性是不是对象显示具有的。
实现代码
// 手写一个浅拷贝
function shallow() {
let newObj = {};
for (let key in obj) {
if (!obj.hasOwnProperty(key)) { // 忽略隐式属性,判断是否是自身显示属性
newObj[key] = obj[key];
}
}
return newObj;
}
console.log(shallow(obj));
使用 hasOwnProperty 方法可以确保只复制对象自身的属性,而不复制继承的属性。这个方法对于确保浅拷贝的正确性非常重要,因为它避免了无意中复制对象原型链上的属性。
深拷贝
深拷贝是基于原对象创建一个完全独立的新对象,拷贝过程中复制原对象及其所有嵌套对象和数组的内容。这意味着,原对象和新对象完全独立,互不影响。
深拷贝不仅复制对象的第一层属性,还会递归复制所有嵌套对象和数组的内容,确保新对象与原对象在内存中完全独立。即使原对象中的嵌套对象发生变化,新对象也不会受到影响。
常见深拷贝方法
JSON.parse(JSON.stringify(obj))
const original = { a: 1, b: { c: 2 } };
const copy = JSON.parse(JSON.stringify(original));
console.log(copy); // 输出 { a: 1, b: { c: 2 } }
通过 JSON.stringify 方法将对象序列化为 JSON 字符串,再通过 JSON.parse 方法将 JSON 字符串解析为新对象。这种方法适用于没有函数、undefined、Symbol、循环引用和 BigInt 的对象。
structuredClone(obj):
const original = { a: 1, b: { c: 2 } };
const copy = structuredClone(original);
console.log(copy); // 输出 { a: 1, b: { c: 2 } }
这是现代 JavaScript 提供的内置方法,可以深拷贝对象,包括处理循环引用和更复杂的数据结构,如 Map 和 Set。
手写一个深拷贝方法
深拷贝的实现需要递归遍历原对象的所有属性,确保所有嵌套对象和数组都被独立复制
实现原理
-
借助for in 遍历原对象,将原对象的属性值增加在新对象中。
-
因为 for in 会遍历到对象隐式具有的属性,通常要使用 obj.hasOwnProperty(key) 来判断拷贝的属性是不是对象显示具有的。
-
如果遍历到的属性值是原始值类型,直接往新对象中赋值,如果是引用类型,递归创建新的子对象。
实现代码
const user = {
myName: {
first: 'John',
last: 'Doe'
},
age: 30,
}
function deep(obj) {
let newObj = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) { // 只拷贝显示具有的属性
if (obj[key] instanceof Object) { // typeof obj[key] === 'object' && obj[key] !== null
newObj[key] = deep(obj[key]); // 递归拷贝,对象里的key为对象,则继续递归拷贝对象
} else {
newObj[key] = obj[key];
}
}
}
return newObj;
}
const newUser = deep(user);
user.myName.first = 'Tom'; // newUser.myname.first不会受影响
console.log(newUser); // 输出{ myName: { first: 'John', last: 'Doe' }, age: 30 }
通过上述方法,我们可以在JavaScript中实现对对象和数组的深拷贝。这些方法在需要创建完全独立的对象副本,以防止原对象变化影响新对象时非常有用。深拷贝特别适用于处理复杂数据结构和确保数据独立性的场景。
总结
好的,到这里相信大家对浅拷贝和深拷贝都有了一个比较深刻的认识,但内容过多,可能还有不清楚的,这里我给大家做一个总结,总结如下表:
| 特性 | 浅拷贝 | 深拷贝 |
|---|---|---|
| 定义 | 只复制对象的第一层属性,嵌套的引用类型仍指向原对象 | 递归复制所有层级的属性,生成完全独立的新对象 |
| 使用场景 | - 性能需求较高的场景 - 数据结构简单 - 只需部分独立的场景 | - 需要数据完全独立 - 复杂数据结构 - 避免副作用 |
| 常见方法 | - Object.create(x)- Object.assign({}, a)- [].concat(arr)- 数组解构 ...a- arr.slice(0)- arr.toReversed().reverse() | - JSON.parse(JSON.stringify(obj))- structuredClone(obj) |
| 优点 | - 性能高 - 实现简单 | - 数据独立 - 适用范围广 |
| 缺点 | - 共享引用,嵌套对象修改会影响原对象 - 适用范围有限 | - 性能较低,处理复杂结构时更慢 - 实现复杂,尤其在处理循环引用时 |
| 实现原理 | 遍历第一层属性,并将其复制到新对象中 | 递归遍历所有属性,处理引用类型和原始值类型,确保完全独立 |
以上便是呆同学分享给大家的关于JavaScript中的浅拷贝和深拷贝技术知识,希望能够帮助到你,如果觉得有用,还望点赞收藏+关注哦~