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

前言

先聊一下关于数据的存储,数据分为两大类:原始数据类型和引用数据类型,原始数据类型有七类: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)

相关推荐
阿珊和她的猫3 小时前
v-scale-scree: 根据屏幕尺寸缩放内容
开发语言·前端·javascript
PAK向日葵5 小时前
【算法导论】PDD 0817笔试题题解
算法·面试
加班是不可能的,除非双倍日工资7 小时前
css预编译器实现星空背景图
前端·css·vue3
wyiyiyi8 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip8 小时前
vite和webpack打包结构控制
前端·javascript
excel8 小时前
在二维 Canvas 中模拟三角形绕 X、Y 轴旋转
前端
阿华的代码王国9 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
一条上岸小咸鱼9 小时前
Kotlin 基本数据类型(三):Booleans、Characters
android·前端·kotlin
Jimmy9 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
ZXT9 小时前
promise & async await总结
前端