浅拷贝与深拷贝:数据复制的孪生策略

前言

先聊一下关于数据的存储,数据分为两大类:原始数据类型和引用数据类型,原始数据类型有七类:number、string、boolean、undefined、null、Symbol、BigInt;引用数据类型有:function、{}、date、[]等。那么他们的存储方式有什么区别呢?

  • 原始数据类型,js引擎会将它直接存储在栈里面
  • 引用数据类型:js引擎会将它的引用地址存储在栈里,值存储在堆里

代码实例一:

ini 复制代码
let a = 1;
let b = a;
a = 2;
console.log(b);

分析: 原始类型的值,js引擎会将它直接存储在栈里面,所以定义一个数字类型的变量a,js引擎会在栈中为它分配一个内存空间,将a赋值给另外一个变量b时,js引擎会在栈中为b分配一个内存空间,并将a的值分配给b,所以修改a的值不会影响b。

代码实例二:

ini 复制代码
let a = {
    name:'ltt'
}
let b = Object.create(a);
a.name = 'lxp';

console.log(b.name);

分析: 引用类型的值,js引擎会在堆中为其分配一段内存空间,然后将地址存放在栈中,所以存放在栈中的为a的地址,创建b,将a赋值给b,复制的为a的地址。所以当a发生改变的时候,b也会发生改变。

正文

浅拷贝(Shallow Copy)

定义

创建一个新对象或数组作为原对象或数组的副本,但这个副本与原对象或数组中的引用类型成员(如子对象、数组等)共享相同的内存地址 。换言之,浅拷贝仅仅复制了对象的第一层(顶层)的值,如果对象的属性是基本数据类型(如数字、字符串、布尔值等),这些值会被复制;而如果属性是引用类型(如对象、数组),则复制的是这些引用的地址而非实际的引用对象。因此,原对象和副本对象中的引用类型成员是相互影响的,修改其中一个对象的引用类型成员会影响到另一个对象。

常见的浅拷贝方法

Object.create(x)

create()创建一个新对象,将a对象复制给该新对象,为浅拷贝,a为引用类型,所以将a赋值给b只是复制了该对象的第一层,即a、b共享相同的内存地址,所以修改a的值,b也会受影响:

ini 复制代码
let a = {
    name:'ltt'
}
let b = Object.create(a);
a.name = 'lxp';

console.log(b.name);

Object.assign({}, a)

css 复制代码
Object.assign(target, ...sources)
  • target:目标对象,即要被赋值的对象。
  • ...sources:源对象,一个或多个对象,它们的可枚举属性会被复制到目标对象中。

示例

assign()将对象a复制给目标对象{},为浅拷贝,name为原始数据类型,值会被直接复制,like为引用数据类型,复制的为like的第一层,即like的内存地址,所以修改name不会影响c,修改like会影响c:

ini 复制代码
let a = {
    name:'ltt',
    like: {
        n:'素~'
    }
}
let c = Object.assign({}, a);
a.name = 'lxp';
a.like.n = '亚比,囧囧囧~';

console.log(c);

[].concat(arr)

通过concat将arr与[]连接起来,返回一个新数组,也就相当于把arr复制给newArr,这种复制的方式为浅拷贝,1,2,3都为原始数据类型,会直接复制,所以当修改arr的时候,newArr不会发生改变。

ini 复制代码
let arr = [1, 2, 3];
let newArr = [].concat(arr);// 将arr中的元素和[]的元素合并,并返回到一个新数组中

arr.splice(1, 1);
arr[1] = 20;

console.log(newArr);

数组解构 [...arr]

...arr\]将arr复制给newArr,这种复制为浅拷贝,`1,2,3`为原始数据类型,会直接复制,`[4]`为引用数据类型,会把第一层复制给newArr,即该对象的内存地址,所以改变arr\[2\]不会影响,改变arr\[3\]\[0\]会影响。 ```ini let arr = [1, 2, 3, [4]]; let newArr = [...arr]; arr[2] = 30; arr[3][0] = 40; console.log(newArr); ``` ![image.png](https://file.jishuzhan.net/article/1793199315183734785/b2012113ebad9f5dcf69a763b5ec22a3.webp) #### arr.slice(0) ```sql array.slice([start[, end]]) ``` * `start`:提取开始的索引(**包含**该位置的元素)。如果为负数,则表示从数组末尾开始计算的位置。默认为0。 * `end`:提取结束的索引(**不包含**该位置的元素)。如果省略或为数组长度,则提取至数组末尾。如果为负数,同样表示从数组末尾开始计算的位置。 arr.slice(0)表示从数组的下标0开始,提取至数组末尾,也就是将数组arr复制给s,这种复制为浅拷贝,`1,2,3`为原始数据类型,直接复制,修改arr中的这些数据,不会影响s,`{a:10}`为引用数据类型,复制第一层,即该对象的内存地址,所以修改arr中的该对象会影响s。 ```ini let arr = [1, 2, 3, {a: 10}] let s = arr.slice(0); arr[1] = 20; arr[3].a = 100; console.log(s); console.log(arr); ``` ![image.png](https://file.jishuzhan.net/article/1793199315183734785/6aaf2d0662ccc194f3f76054e318f4f1.webp) #### arr.toReversed().reverse() `arr.toReversed()` 的效果是得到一个与原数组元素顺序相反的新数组,而原数组保持不变,然后通过调用 `reverse()` 反转数组,使其顺序回到最初的状态,也就达到了将arr复制给s,这种复制为浅拷贝。 ```ini let arr = [1, 2, 3, {a: 10}] let s = arr.toReversed().reverse(); arr[1] = 20; arr[3].a = 100; console.log(s); console.log(arr); ``` 如果终端出现下面的错误,是因为node版本低了,可以去控制台运行查看结果 ![image.png](https://file.jishuzhan.net/article/1793199315183734785/f9b235cb5f930a98903df830bfebc50c.webp) ![image.png](https://file.jishuzhan.net/article/1793199315183734785/d4c40a81665e2e7beb42c754c79300a9.webp) ### 手写浅拷贝 for in会遍历到隐式属性: ```ini Object.prototype.d = 4; let obj = { a: 1, b: 2, c: 3 } for (let key in obj) { console.log(obj[key]); } ``` ![image.png](https://file.jishuzhan.net/article/1793199315183734785/5aa854837565d041bd09ccc9a34039de.webp) 创建一个新对象newObj,通过for in循环遍历obj,将其复制给newObj,for in会遍历到obj的隐式属性,所以在复制给newObj之前要先通过obj.hasOwnProperty(key)检查是不是obj自己具有的属性。 ```ini let obj = { name: 'ltt', like: { a: 'eat' } } function shadow(obj) { let newObj = {}; for (let key in obj) { if (obj.hasOwnProperty(key)) { newObj[key] = obj[key]; } } return newObj; } let newObj = shadow(obj); obj.name = 'lxp'; obj.like.a = 'running'; console.log(newObj); ``` ![image.png](https://file.jishuzhan.net/article/1793199315183734785/a8836a5bc8cfaffd508ddaf238ec4d01.webp) ### 实现原理 * 借助for in 遍历原对象,将原对象的属性值增加到新对象中 * 因为for in 会遍历到对象隐式具有的属性,通常要使用 * obj.hasOwnProperty(key)来判断要拷贝的属性是不是对象显示具有的 ## 深拷贝(Deep Copy) ### 定义 深拷贝则是创建一个**完全独立的新对象或数组** ,这个新对象不仅复制了原对象的所有第一层属性值,而且递归地复制了所有嵌套的引用类型的属性,直到所有的层级都是**全新的副本** 。这意味着深拷贝的对象与原对象在内存中是完全独立的,即使内部包含复杂的数据结构,**修改深拷贝后的对象也不会影响到原对象及其子对象**。 ### 常见的浅拷贝方法 #### JSON.parse(JSON.stringify(obj)) 这种复制方式为深拷贝,拷贝后的新对象与原对象互相独立,所以修改原对象不会影响新对象 ```javascript let obj = { name: 'ltt', age: 18, like: { n:'run' }, a: true, b: undefined, c:null, d: Symbol(1), f: function() {} } let obj2 = JSON.parse(JSON.stringify(obj)); obj.like.n = 'eat'; console.log(obj); console.log(obj2); ``` ![image.png](https://file.jishuzhan.net/article/1793199315183734785/0b94cb4a4ee5c92805678d677d4c2dbe.webp) 当进行循环引用的时候会报错: ```ini let obj = { name: 'ltt', age: 18, like: { n:'run' }, a: true, b: undefined, c:null, d: Symbol(1), f: function() {} } obj.c = obj.like; obj.like.m = obj.c; let obj2 = JSON.parse(JSON.stringify(obj)); obj.like.n = 'eat'; console.log(obj); console.log(obj2); ``` ![image.png](https://file.jishuzhan.net/article/1793199315183734785/e0ae73ffc84a6fd56f59c5759108888a.webp) **总结:** 1. 不能识别BigInt类型 2. 不能拷贝undefined、Symbol、function类型的值 3. 不能处理循环引用 #### structuredClone() 这种复制方式为深拷贝,拷贝后的新对象与原对象互相独立,所以修改原对象不会影响新对象 ```ini const user = { name: { firstName: 'tt', lastName: 'l' }, age: 18 } const newUser = structuredClone(user); user.name.firstName = 'xp'; console.log(newUser); ``` 可能会在终端报错,和上面一样,node版本的问题 ![image.png](https://file.jishuzhan.net/article/1793199315183734785/8057e4e2bed000e286e38d169d62192c.webp) ![image.png](https://file.jishuzhan.net/article/1793199315183734785/9b4ea046195c950ea59c3879cbc5b8a3.webp) ### 手写深拷贝 * 通过for in遍历数组 * 当遍历到数组时采用递归再次遍历知道不为数组再复制 * 否则直接复制 ```ini const obj = { name: { firstName: 'tt', lastName: 'l' }, age: 18 } 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]); }else{ newObj[key] = obj[key]; } } } return newObj; } let obj2 = deep(obj); obj.name.firstName = 'xp'; console.log(obj); console.log(obj2); ``` ![image.png](https://file.jishuzhan.net/article/1793199315183734785/c8a4074dea7b750268eac07d8731ea80.webp) ### 实现原理 * 借助for in 遍历原对象,将原对象的属性值增加到新对象中 * 因为for in 会遍历到对象隐式具有的属性,通常要使用 obj.hasOwnProperty(key)来判断要拷贝的属性是不是对象显示具有的 * 如何遍历到的属性值为原始值类型,直接往新对象中赋值,如果是 引用类型,递归创建新的子对象 # 结语 浅拷贝和深拷贝的概念、常用方法和手写方法都理解了吗?记得来回复习哦,温故而知新,可以为师矣\~ ![image.png](https://file.jishuzhan.net/article/1793199315183734785/bf392f8f6adb50321b437f9ff541d696.webp)

相关推荐
LaoZhangAI1 小时前
2025最全GPT-4o图像生成API指南:官方接口配置+15个实用提示词【保姆级教程】
前端
ONE_Gua1 小时前
chromium魔改——CDP(Chrome DevTools Protocol)检测01
前端·后端·爬虫
LaoZhangAI1 小时前
2025最全Cherry Studio使用MCP指南:8种强大工具配置方法与实战案例
前端
咖啡教室1 小时前
前端开发日常工作每日记录笔记(2019至2024合集)
前端·javascript
探索为何1 小时前
JWT与Session的实战选择-杂谈(1)
后端·面试
溪饱鱼1 小时前
Nuxt3能上生产吗?
前端
咖啡教室1 小时前
前端开发中JavaScript、HTML、CSS常见避坑问题
前端·javascript·css
怒放吧德德2 小时前
MySQL篇:MySQL主从集群同步延迟问题
后端·mysql·面试
LaoZhangAI3 小时前
Claude MCP模型上下文协议详解:AI与外部世界交互的革命性突破【2025最新指南】
前端
LaoZhangAI3 小时前
2025最全Cursor MCP实用指南:15个高效工具彻底提升AI编程体验【实战攻略】
前端