深拷贝与浅拷贝你知多少?

608 阅读7分钟

前言:

深拷贝和浅拷贝都是js中挺重要的知识点,这俩个东西在面试时也基本会被面试官问到,而且很可能会让你手写一下两个拷贝方法。

在js中拷贝通常都是针对引用数据类型而言,咱们先来看看深浅拷贝分别是啥样的

let a = 1
let b = a
a = 2
console.log(b) // 1

这里当a赋值给b后,你在去修改a的值,你会发现b的值不会受a的改变而改变,这里传的是a的值而非a的地址,其实这段代码里面也是有一些底层逻辑的,也就是js的预编译,大家可以去看看我的这篇文章——js的预编译

像这样,一个数据拷贝给另一个数据,原数据改变不会影响到另一个数据,就是深拷贝。基于原对象,拷贝得到一个新的对象,原对象中内容的修改不会影响新对象。

来看下面这个:

let obj1 = {
	age: 18
}
let obj2 = obj1
obj1.age = 20
console.log(obj2.age) // 20

引用类型在调用栈中存放的是地址,传的是地址而并非是值,引用类型的数据真正存放在堆中,当obj1赋值给obj2时是给的一个地址,obj1和obj2共用一个地址,你的数据改变我的数据也会跟着改变。

像上面这样,一个数据拷贝给另一个数据,原数据的改变会影响到另一个数据,就是浅拷贝。基于原对象,拷贝得到一个新的对象,原对象中内容的修改会影响新对象。

通常在实际开发的过程中咱们也会遇到一些需求需要将一个对象拷贝过来使用,那有没有一些手段能够实现拷贝的方法呢,别急,我慢慢跟你说,咱们先来介绍一下浅拷贝的实现方法:

浅拷贝

浅拷贝主要有6种实现方法,下面两种是对象的浅拷贝常见方法:

方法一:Object.creat(x)

这种方式通过创建一个新对象,并将原始对象的属性复制到新对象中来进行浅拷贝。

let a = {
    name: "毛毛",
    like: {
        n: "running"
    }
}
let b= Object.create(a);
a.name = "付哥";
console.log(b.name);

image-20240523180911883.png 这里我们可以看到,当我们创建一个a对象后,通过Object.creat(a)拷贝给b后,然后去修改a对象里的name属性,再去输出拷贝到b里面的name属性,可以看到也是跟着一起变化,一起修改了的,这就是浅拷贝。

方法二:Object.assign({},a)

使用Object.assign()方法可以将一个或多个源对象的属性复制到目标对象中。

Object.assign(target, ...sources)

1.target:目标对象,即要被赋值的对象。

2...sources:源对象,一个或多个对象,他们的属性可以被赋到目标属性中

举个例子:

let a = {
    name: "毛毛",
    like: {
        n: "running"
    }
}
let c=Object.assign({},a);
a.name="徐总";
a.like.n="swimming";
console.log(c);//里面的对象属性会修改,因为引用的是地址

image-20240523182314475.png

这里咱们定义了一个a对象后,在里面定义了一个name属性和一个like对象,通过Object.assign({},a)方法将它拷贝给c之后再去修改a对象里面的name属性和like对象,我们可以看到c里面的like对象属性里的值改变了,但是name属性没有改变,这是为啥呢?

assign()将对象a复制给目标对象{},为浅拷贝,name为原始数据类型,值会被直接复制,like为引用数据类型,复制的为like的引用内存地址,所以修改name不会影响c,修改like则会影响c。哦,原来是这样。

在数组中也有浅拷贝的手段,下面四个是数组的浅拷贝:

方法三:[].Concat(x):

使用数组的concat()方法可以将一个或多个数组连接起来,并返回一个新的数组。

let arr = [1, 2, 3, {a: 10}]
let newArr = [].concat(arr)
arr[3].a = 1
console.log(newArr) // [ 1, 2, 3, { a: 1 } ]

可以看到确实可以拷贝且修改相应值,没有问题。

方法四:数组的解构 newArr=[...arr]

[...arr]数组解构是es6新增的方法,解构是从数组中提取元素进行赋值

let arr = [1, 2, 3, {a: 10}]
let newArr = [...arr]
arr[3].a = 1
console.log(newArr) // [ 1, 2, 3, { a: 1 } ]

可以看到也没问题。

方法五:arr.slice(0)

提取数组的一部分,返回一个新数组,不会修改原数组,第一个参数是起始位置,第二个参数是结束位置,左闭右开, 截取出来其中一部分元素

let arr = [1, 2, 3, {a: 10}]
let newArr = arr.slice(0)
// 只有一个参数0,也就是提取整个数组
arr[3].a = 1
console.log(newArr) // [ 1, 2, 3, { a: 1 } ]

方法六:arr.toReversed().reverse()

let arr=[1,2,3,{a:10}];
let res =arr.toReversed().reverse();
 console.log(res);

image-20240523184346254.png

也没问题。那咱们能不能自己手搓一个浅拷贝的实现方法呢?没问题,直接上手。

手搓一个浅拷贝的实现方法:

思路:

  1. 首先借助for in 遍历原对象,将原对象的属性值增加在新对象中
  2. 因为 for in 会遍历到对象隐式具有的属性,通常要使用obj.hasOwnProperty(key)来判断要拷贝的属性是不是对象显示具有的
//手搓一个浅拷贝的方法
let obj = {
    name: "罗总",
    like: {
        a: "food"
    }
}
function shallowCopy(obj) {
    let newObj = {};
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {//判断是否是自身属性
            newObj[key] = obj[key];
        }
    }
    return newObj;
}
let obj2 = shallowCopy(obj);
obj2.like.a = "beer";
console.log(obj2);

image-20240523184900848.png 完美!

深拷贝

深拷贝是指创建一个全新的对象,并复制原始对象的所有值和嵌套对象,而不是仅仅复制引用。这样,新对象在内存中有一个独立的属性,对其中任何对象的修改都不会影响到其他对象。

法一:深拷贝只有一种自带的方法:JSON.parse(JSON.stringify(obj))

json是前后端数据传输的一种数据交互格式,类似对象,它的key都是字符串。

JSON.stringify(obj):这个方法将对象转换成JSON字符串格式

let  obj={
    name: "John",
    age:18,
    like:{
        n:"cooding"
    },
    a:true,
    b:undefined,
    c:null,
    d:Symbol(1),
    //e:BigInt(100),
    f:function(){}
}
let obj2=JSON.parse(JSON.stringify(obj));
console.log(obj2);

image-20240523232331938.png

可以看到确实可以拷贝,但是这玩意有几个缺点:

缺陷:

1.不能识别BigInt类型

2.不能拷贝 undefined Symber function等类型

3.不能处理循环引用问题

法二:structuredClone()

这种复制方式为深拷贝,拷贝后的新对象与原对象互相独立,所以修改原对象不会影响新对象。也是目前最好的深拷贝方法。

const user ={
    name:{
        firstName: "牛",
        lastName: "蜗"
    },
    age:18
}
const newUser =structuredClone(user)
user.name.firstName = "牛牛"
console.log(newUser)

image-20240523233935656.png

可以看到使用structuredClone()方法拷贝给newUser后,再去修改user里面的值,newUser里面的值没有发生变化,确实是深拷贝。

手搓一个深拷贝实现方法:

- 实现原理:

  1. 借助for in 遍历原对象,将原对象的属性值增加在新对象中
  2. 因为 for in 会遍历到对象隐式具有的属性,通常要使用obj.hasOwnProperty(key)来判断要拷贝的属性是不是对象显示具有的
  3. 如果遍历到的属性值是原始值类型,直接往新对象中赋值,如果是引用类型,递归创建新的子对象
const user ={
    name:{
        firstName: "牛",
        lastName: "蜗"
    },
    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;
}
const newUser = deep(user);
user.name.firstName = "牛牛";
console.log(newUser)

在for循环中:

  1. if(obj.hasOwnProperty(key)){}:检查当前属性key是否是obj自身的属性,而不是从原型链继承来的。
  2. if(obj[key] instanceof Object){}:检测obj中键为key的属性值是否是一个对象(注意:这也会包括数组、函数等其他类型的对象)。使用instanceof操作符来判断。
  3. newObj[key]=deep(obj[key]);:如果obj[key]确实是一个对象,则递归调用同一个拷贝函数(假设为deep),并将拷贝结果赋值给新对象newObj的相应属性。这是深拷贝的核心,因为它能够处理多层嵌套的对象结构。
  4. else{ newObj[key]=obj[key]; }:如果obj[key]不是一个对象,则直接将属性值复制到newObj中。

好啦。深拷贝与浅拷贝就聊到这里了,屏幕前的你是否还有其他方法呢?欢迎评论区留言交流。