深拷贝与浅拷贝是什么?如何实现?

278 阅读6分钟

在 JavaScript 中,深拷贝(Deep Copy)和浅拷贝(Shallow Copy)是用于复制对象或数组的两种不同方式。

浅拷贝是指创建一个新的对象或数组,然后将原始对象或数组的引用复制给新对象或数组。这意味着新对象或数组与原始对象或数组共享相同的内部数据,当修改其中一个对象或数组时,另一个也会受到影响。换句话说,浅拷贝只复制了对象或数组的引用,而不是实际的值或内容。

深拷贝则是创建一个全新的对象或数组,并递归地复制原始对象或数组中的所有值和内容。这意味着深拷贝生成的对象或数组与原始对象或数组完全独立,彼此之间没有任何关联。修改其中一个对象或数组不会影响另一个。

1.在讲上面的知识点之前,我先举个不太恰当的例子(小伙伴们可以先想想再看我的解释哦)

//示例代码:
    let a = 5
     let b = a
     b = 3
     console.log(a,b) // 5 3

如果我问下各位小伙伴这个是深拷贝还是浅拷贝呢?哈哈哈,可能细心的小伙伴就会发现我上面对于这两种拷贝方式的的定义的介绍时,都提到了它们在对象或者数组的前提下讨论的,我上面写的是基本的数据类型,没有深浅拷贝的说法,而是解释为赋值操作较为准确,如果非要从严格意义上来讲的话,这个应该可以说是深拷贝,因为我将a的值赋值给b之后,修改了b的值,而这并不会影响到a原来的值,也就是说它们之间没有任何联系。

2.那么我现在举一个引用数据类型的例子

//示例代码:
    let arr = [1,2,3]
    let newArr = arr
    newArr.push(4)
    console.log(arr,newArr) //[1,2,3,4] [1,2,3,4]

上面对于newArr的修改,arr也会跟着发生变化,这个例子就是一个典型的浅拷贝

3.我再问大家一个问题哈:解构赋值是深拷贝还是浅拷贝呢?下面我用两个例子给小伙伴们解释清楚。

//示例代码1:
    let arr = [1,2,3]
    let newArr = [...arr]
    newArr.push(4)
    console.log(arr,newArr) //[1,2,3] [1,2,3,4]

//示例代码2:
    let arr1 = [[1,2,3],[4,5,6]]
    let newArr1 = [...arr]
    newArr1[0].push(7)
    console.log(arr,newArr) //[[1,2,3,7],[4,5,6]] [[1,2,3,7],[4,5,6]]

通过这两个例子不难看出,一维数组和对象的解构赋值可以看作是深拷贝,多维的解构赋值就是浅拷贝

4.现在我来介绍一下我们常用的方法来实现深浅拷贝:

浅拷贝:

//方法1:扩展运算符
//对象
    const Obj = { name: 'John', age: 25 }
    const shallowCopyObj = { ...Obj }
    console.log(shallowCopyObj) // { name: 'John', age: 25 }
    // 修改原始对象的属性
    Obj.name = 'Jane'
    console.log(Obj) // { name: 'Jane', age: 25 }
    console.log(shallowCopyObj) // { name: 'John', age: 25 }

//数组
    const Arr = [1, 2, 3]
    const shallowCopyArr = [...Arr]
    console.log(shallowCopyArr) // [1, 2, 3]
    // 修改原始数组
    originalArr[0] = 10
    console.log(Arr) // [10, 2, 3]
    console.log(shallowCopyArr) // [1, 2, 3]

看到这里,有的小伙伴可能就发现问题了:撕~,为什么这个是浅拷贝呢?它们不是互不影响吗?其实在这个示例中,使用扩展运算符创建了一个新的对象shallowCopyObj,并将原始对象Obj的属性复制给它,这是一种浅拷贝的方式,因为新对象只是复制了原始对象的引用,而不是递归地复制对象的所有值和内容,数组也同理,将原始数组Arr的元素复制到新数组shallowCopyArr中。尽管我们修改了原始数组的第一个元素,新数组并没有受到影响,但是如果原始数组中的元素是对象或数组,那么它们仍然是通过引用共享的,因此是浅拷贝。这里就不得不和上面的第三点的解构赋值区分一下了,以防有的小伙伴搞混:对象解构赋值和数组解构赋值都进行了深拷贝,因为复制的是属性值或元素值,而不是引用,所以进行的是深拷贝。

//方法2:Object.assign()方法(用于对象)
    const Obj = { name: 'John', age: 25 }
    const shallowCopyObj = Object.assign({}, Obj)
    console.log(shallowCopyObj) // { name: 'John', age: 25 }
    // 修改原始对象的属性
    Obj.name = 'Jane'
    console.log(Obj) // { name: 'Jane', age: 25 }
    console.log(shallowCopyObj) // { name: 'John', age: 25 }
//方法3:concat()方法(用于数组)
    const Arr = [1, 2, 3]
    const shallowCopyArr = Arr.concat()
    console.log(shallowCopyArr) // [1, 2, 3]
    // 修改原始数组
    Arr.push(4)
    console.log(Arr) // [1, 2, 3, 4]
    console.log(shallowCopyArr) // [1, 2, 3]

对于方法3,我补充一点知识(参考MDN官网):Object.assign()方法主要用于对象的合并和复制Object.assign()方法将源对象的属性复制到目标对象中,并返回目标对象(注:第一个参数为目标对象,后面的参数为源对象)。它接受一个或多个源对象作为参数,并将它们的属性复制到第一个参数所指定的目标对象中。如果目标对象与源对象具有相同的键(属性名)则目标对象中的属性将被源对象中的属性覆盖,后面的源对象的属性将类似地覆盖前面的源对象的同名属性。这里我使用一个空对象作为目标对象,将源对象的属性复制到里面。

深拷贝:

方法1JSON.stringify() + JSON.parse()
    const Obj = {
        name: '张三',
        age: 18,
        hobbies: ['学习','吃饭']
    }
    const deepCloneObj = JSON.parse(JSON.stringify(Obj))
    console.log(deepCloneObj) // {name: '张三',age: 18,hobbies: ['学习','吃饭']}
    // 修改原始对象
    Obj.name = '李四'
    Obj.hobbies[0] = '睡觉'
    console.log(Obj) //{name: '李四',age: 18,hobbies: ['睡觉','吃饭']}
    console.log(deepCloneObj) // {name: '张三',age: 18,hobbies: ['学习','吃饭']}

方法1虽然可以实现深拷贝,但是它有一定的缺陷:无法复制函数。现在我来分享另外一种标准的深拷贝的方法

方法2:递归复制:使用递归遍历对象或数组的每个属性或元素,并创建一个新的对象或数组来复制每个值。
    function deepCloneObj(source) {
        const targetObj = source.constructer === Array ? [] : {}
        for(keys in source) {
            if(source[keys] && typeof source[keys] === 'object') {
                //引用数据类型
                targetObj[keys] = deepCloneObj(source[keys])
            } else {
                //基本数据类型数据直接赋值即可
                targetObj[keys] = source[keys]
            }
        }
        return targetObj
    }
    let obj = {
        name: '张三',
        hobbies: ['学习','吃饭'],
        innerObj: {
            age: 18,
            sex: '男'
        }
    }
    const newObj = deepCloneObj(obj)
    // {name: '张三',hobbies: ['学习','吃饭'],innerObj: { age: 18,sex: '男'}}
    console.log(newObj)
    //修改原始对象
    obj.hobbies[0] = '睡觉'
    obj.innerObj.age = 20
    console.log(obj)
    // {name: '张三',hobbies: ['睡觉','吃饭'],innerObj: { age: 20,sex: '男'}}
    console.log(newObj)
    // {name: '张三',hobbies: ['学习','吃饭'],innerObj: { age: 18,sex: '男'}}

现在我补充一点上面方法2的知识,促进理解:首先函数中根据源对象的构造函数constructor来创建一个空的目标对象。在JavaScript中,每个对象都有一个构造函数属性constructor,它指向创建该对象的函数。通过检查源对象的构造函数,可以确定它是一个数组还是一个普通对象,简单来说就是如果它是一个数组,那么这个constructor就会指向构造它的Array这个基类,普通对象就指向Object这个基类,并相应地创建一个空的目标对象。通过这种方式,可以根据源对象的类型动态创建相应类型的目标对象,以便进行拷贝操作。

本文到这里就讲完啦!如果有错误的话,烦请大佬在评论区指正谢谢。