深入了解深拷贝和浅拷贝
前言
只要新对象和原对象一模一样,就叫作拷贝。拷贝操作一般针对于处理引用类型(如对象、数组、函数等),而不涉及基本类型(如数字、字符串、布尔值、null、undefined),原因在于基本类型和引用类型在内存中的存储方式不同。
基本类型
基本类型的变量直接存储在栈结构中。当把基本类型的值赋给另一个变量时,会在栈内存中创建一个新的空间来存储这个值,两个变量之间互不影响。例如:
let a = 10;
let b = a; // 这里是值传递,b获得的是a的值的一个副本
a = 20;
console.log(b); // 输出10,因为b的值并未因a的改变而改变
引用类型
而对于引用类型,变量存储的是指向堆内存中实际数据的地址。当进行简单的赋值操作时,实际上是复制了这个引用地址,而不是数据本身。这意味着两个变量最终指向的是同一个内存地址上的数据,修改其中一个会影响另一个。例如:
let obj1 = {
age : 18
}
let obj2 = obj1;// obj2获得了obj1引用地址的一个副本
obj1.age = 23;
console.log(obj2.age);// 输出23,因为obj1和obj2指向同一对象
浅拷贝与深拷贝的必要性
正因为引用类型的这种特性,当需要复制一个对象或数组但又不希望原对象被修改影响到副本时,就需要使用浅拷贝或深拷贝来创建对象的新实例。浅拷贝只复制一层,对于嵌套的引用类型仍然保持引用关系;深拷贝则递归地复制所有层级,确保新旧对象完全独立。
浅拷贝
基于原对象,拷贝得到一个新对象,原对象中内容的修改会影响新对象。
根本原因:对于原对象中的基本类型属性(如数字、字符串、布尔值等),浅拷贝会直接复制其值;对于引用类型属性(如数组、对象等),浅拷贝只会复制它们的内存地址引用,而不是这些引用所指向的数据结构本身。
常见的浅拷贝方法
1. Object.create( )
let a = {
name : '阿年'
}
let b = Object.create( a );// 创建一个新的对象,隐式继承原对象
a.name = '阿美';
console.log( b.name );// 输出 阿美
通过 Object.create()可以创建一个新的对象,隐式继承原对象,但当对象 a 中的值发生变化时, b 也会随之变化。
2. Object.assign({ },a)
let a = {
name : '昔年',
age: 18
}
let c = Object.assign({ },a);// 会影响a
console.log( c );// 输出 {name : '昔年', age: 18}
Object.assign方法主要用于合并对象,它允许你将一个或多个源对象的属性的值复制到目标对象中。这个方法是浅拷贝,适用于简单属性的合并。
3. [ ].concat(arr)
let arr = [1, 2, 3];
let newArr = [ ].concat( arr );// 将arr中的元素合并到[]中,并返回一个新数组
console.log( newArr );// 输出 [1, 2, 3]
使用数组的concat方法可以合并一个或者多个数组,并返回一个新数组
4. 数组的解构[...arr]
let arr = [1, 2, 3];
let newArr = [ ...arr ];
console.log( newArr );// 输出 [1, 2, 3]
[...arr] 是一种使用扩展运算符(Spread Operator)进行数组解构和浅拷贝的方法。这种语法可以将数组arr的所有元素展开并创建一个新数组。具体来说,它会复制arr数组的所有元素到一个新的数组中。这个过程是浅拷贝,对于数组中的基本类型数据会复制值,而对于对象(引用类型)则复制引用。
5. arr.slice[0]
let arr = [1, 2, 3];
let newArr = arr.slice( 0 );
console.log(newArr);// 输出 [1, 2, 3]
数组中的slice() 返回数组的一部分,不会修改原始数组。
6. arr.toReversed( ).reverse( );
let arr = [1, 2, 3];
let res = arr.toReversed().reverse();
console.log(res);
两极反转,arr.toReversed() 的效果是得到一个与原数组元素顺序相反的新数组,而原数组保持不变,然后通过调用 reverse() 反转数组,使其顺序回到最初的状态,也就达到了将arr复制给res,这种复制为浅拷贝。
手写浅拷贝方法
浅拷贝的实现原理:
- 借助 for in 遍历原对象,将原对象的属性增加在新对象中。
- 因为 for in 会遍历到对象隐式具有的属性,通常要使用 obj.hasOwnProperty(key)来判断要拷贝的属性是不是对象显示具有的。
function shallow(obj){
let newObj = {}; // 先创建一个新对象
for(let key in obj){// 遍历原对象
if(obj.hasOwnProperty(key)){ // 判断属性是不是显示具有的
newObj[key] = obj[key]; // 将obj对象的属性逐个复制到newObj对象中。
}
}
return newObj;
}
深拷贝
基于原对象,拷贝得到一个新对象,原对象中内容的修改不会影响新对象
常见的深拷贝方法
1.JSON.parse(JSON.stringify(obj))
let obj = {
name: '昔年',
age: 18,
like: {
n: 'coding'
},
a: true,
b: undefined,
c: null,
d: Symbol(1),
f: function(){}
}
let obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2);
JSON.stringify(obj)
: 这一步将obj对象转换成一个JSON字符串。JSON.parse(字符串)
: 将上一步生成的JSON字符串用JSON.parse方法解析,将其转换回JavaScript对象。这个新对象与原始对象内容相同,但它们在内存中位于不同的位置,即新对象是一个全新的副本,与原对象没有任何引用关系。因此,修改新对象不会影响到原对象,实现了所谓的“深拷贝”。
缺点和限制:
- 不能识别 BigInt类型
- 不能拷贝 undefined 、 Symbol 、 function 类型的值
- 不能处理循环引用
2.structuredClone()
const user = {
name: {
firstname: '牛',
lastname: '蜗'
},
age: 18
}
const newUser = structuredClone(user);
user.name.firstname = '牛牛';
console.log(newUser);
structuredClone() 是一个较新的 JavaScript 函数,它提供了另一种创建对象或值的深拷贝的方法。与 JSON.parse(JSON.stringify(obj)) 不同,structuredClone() 能够处理更多种类的数据类型,并且能够保持对象中的函数、正则表达式(RegExp)、日期(Date)等特殊类型不变,同时支持循环引用的对象结构。
手写深拷贝方法
深拷贝的实现原理:
- 借助 for in 遍历原对象,将原对象的属性增加在新对象中
- 因为 for in 会遍历到对象隐式具有的属性,通常要使用obj.hasOwnProperty(key)来判断要拷贝的属性是不是对象显示具有的。
- 如果遍历到的属性值是原始类型,直接往新对象中赋值,如果是引用类型,递归创建新的子对象。
function deep(obj){
let newObj = {};
for (let key in obj){
if (obj.hasOwnProperty(key)){// 只拷贝显示具有的属性
if (obj[key] instanceof Object){ // 判断 obj[key] 是不是一个对象
newObj[key] = deep(obj[key]);// obj[key] 是一个对象,创建一个新对象,递归
}else{
newObj[key] = obj[key];// obj[key] 不是一个对象,直接赋值
}
}
}
return newObj;
}
总结
今天详细介绍了深浅拷贝的几种常见的方法以及如何手写深浅拷贝,希望可以给你带来帮助。
转载自:https://juejin.cn/post/7372393680596287514