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

前言

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

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);

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版本低了,可以去控制台运行查看结果

手写浅拷贝

for in会遍历到隐式属性:

ini 复制代码
Object.prototype.d = 4;
let obj = {
    a: 1,
    b: 2,
    c: 3
}

for (let key  in obj) {
    console.log(obj[key]);
}

创建一个新对象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);

实现原理

  • 借助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);

当进行循环引用的时候会报错:

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);

总结:

  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版本的问题

手写深拷贝

  • 通过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);

实现原理

  • 借助for in 遍历原对象,将原对象的属性值增加到新对象中
  • 因为for in 会遍历到对象隐式具有的属性,通常要使用 obj.hasOwnProperty(key)来判断要拷贝的属性是不是对象显示具有的
  • 如何遍历到的属性值为原始值类型,直接往新对象中赋值,如果是 引用类型,递归创建新的子对象

结语

浅拷贝和深拷贝的概念、常用方法和手写方法都理解了吗?记得来回复习哦,温故而知新,可以为师矣~

相关推荐
CXDNW几秒前
【网络面试篇】HTTP(2)(笔记)——http、https、http1.1、http2.0
网络·笔记·http·面试·https·http2.0
neter.asia3 分钟前
vue中如何关闭eslint检测?
前端·javascript·vue.js
~甲壳虫3 分钟前
说说webpack中常见的Plugin?解决了什么问题?
前端·webpack·node.js
光影少年22 分钟前
vue2与vue3的全局通信插件,如何实现自定义的插件
前端·javascript·vue.js
As977_23 分钟前
前端学习Day12 CSS盒子的定位(相对定位篇“附练习”)
前端·css·学习
susu108301891126 分钟前
vue3 css的样式如果background没有,如何覆盖有background的样式
前端·css
Ocean☾27 分钟前
前端基础-html-注册界面
前端·算法·html
Rattenking27 分钟前
React 源码学习01 ---- React.Children.map 的实现与应用
javascript·学习·react.js
Dragon Wu29 分钟前
前端 Canvas 绘画 总结
前端
CodeToGym34 分钟前
Webpack性能优化指南:从构建到部署的全方位策略
前端·webpack·性能优化