浅拷贝与深拷贝:JavaScript对象的复制艺术

360 阅读5分钟

在JavaScript中,对象和数组是最常用的引用类型,用于存储复杂的数据结构。当我们在函数间传递或在不同作用域间共享这些数据时,常常需要考虑数据的独立性——即如何确保操作一个拷贝的副本不会影响到原始数据。这便是拷贝技术的用武之地。我们将从理解基本概念入手,然后逐步深入到具体的实现方式及它们之间的区别。

原始类型与引用类型

在开始之前,了解JavaScript中的两种主要数据类型至关重要,在本篇文章之前,我已经通过一篇文章去详细的讲解这两种数据类型了 # 很难吗?十分钟over数据类型、判断和转换 。如果有点忘记了的大佬可以再去看看,不过我们在升深入探讨拷贝之前,再来简单的介绍一些数据类型的存储方式:

  • 原始类型(如字符串、数字、布尔值)可以直接存储在变量中(栈),每个变量都有自己的值。
  • 引用类型(如对象、数组)要存储在内存中的某个位置(堆),变量保存的是指向该位置的引用地址。这意味着两个变量可以指向同一块内存,从而共享数据。

深拷贝与浅拷贝的区别

  • 拷贝:从一个现有的数据实例生成一个新的实例,使得新实例具有与原实例相同的数据状态,也就是我们经常用的Copy或者说复制,拷贝过来的数据与原数据一模一样

  • 浅拷贝:顾名思义,是浅浅的拷贝并不深入。所以只复制对象的第一层属性,对于引用类型的属性,只是复制了该引用类型的引用地址,而非实际的数据。因此,对拷贝后对象的修改可能会影响原对象。

  • 深拷贝:完全复制对象及其所有嵌套的对象,产生一个全新的独立对象,无论怎样修改都不会影响到原对象。对于引用类型的属性,不是只是复制该引用的地址,而是将引用地址所指向的数据全部拷贝过来,用一个新的引用地址去指向,也就是会创建一个新的引用类型去存储这些数据,虽然这些数据与原数据一模一样,但是引用地址不用。如果用===去判断的话会返回false

浅拷贝

浅拷贝的实现方法包括:

  • Object.create():创建一个新对象,其原型为提供的对象。

    let obj = {
      a: 1
    }
    let obj2 = Object.create(obj)
    obj.a=2
    console.log(obj2.a);//输出的 2 -> 浅拷贝
    
  • Object.assign():运用对象中合并两个对象的方法来实现,该方法接受俩个参数,将后者合并到前者。

    let obj = {
      a: 1,
      b: [1, 2]
    }
    let obj2 = Object.assign({}, obj)
    obj.b.push(3)
    console.log(obj2);//输出的 {  a: 1,b: [1, 2, 3]} -> 浅拷贝
    
  • 数组方法:如:合并数组的方法[].concat(结构r)、数组的解构[...arr]、切割数组的方法arr.slice(0)和 数组的顺序倒转arr.toReversed().reverse() ,适用于数组的浅拷贝。

    let arr = [1, 2, 3, {a: 1}]
    
    let arr2 = [].concat(arr)// 输出[ 1, 2, 3, { a: 2 } ]
    let arr2 = [...arr]// 输出[ 1, 2, 3, { a: 2 } ]
    let arr2 = arr.slice(0)// 输出[ 1, 2, 3, { a: 2 } ]
    let arr2 = arr.toReversed().reverse() // 输出[ 1, 2, 3, { a: 2 } ]
    
    arr[3].a = 2
    console.log(arr2);
    
  • 手写一个浅拷贝函数:使用for...in循环结合hasOwnProperty检查,仅复制对象自身的属性。

    function shallowCopy(obj) {
        let newobj = {};
        for (let key in obj) {
          // key 是不是obj显示具有的
          if (obj.hasOwnProperty(key)) {
            newobj[key] = obj[key];
          }
        }
        return newobj;
      }
    
    

然而,浅拷贝不能保证对象内部引用类型属性的独立性。所以一般面试考的不是浅拷贝的手写而是深拷贝的手写,无伤大雅,我们继续。

深拷贝

深拷贝确保了所有层级的属性都是独立的,主要方法包括:

  • JSON.parse(JSON.stringify(obj)):通过序列化再反序列化,但无法识别bigInt类型以及无法拷贝 undefined,function,Symbol这些类型,且无法处理循环引用。

    let obj = {
        a: 1,
        b: {n: 2},
        c: 'cc',
        d: true,
        e: undefined,      //无法识别
        f: null,
        g: function() {},  //无法识别
        h: Symbol(1),      //无法识别
        // i: 123n // 报错:Do not know how to serialize a BigInt
      }
      obj.a = obj.b
      obj.b.m = obj.a  //循环引用  报错:Converting circular structure to JSON
    
      let newObj = JSON.parse(JSON.stringify(obj))
      console.log(newObj);  //输出{ a: 1, b: { n: 2 }, c: 'cc', d: true, f: null }
    
  • structuredClone():现代浏览器中新增的深拷贝方法,能更安全地处理复杂数据结构,包括循环引用,但支持度有限。例如function和Symbol这俩类型无法拷贝

    let obj = {
        a: 1,
        b: {n: 2},
        c: 'cc',
        d: true,
        e: undefined,
        f: null,
        i: 123n,
        // g: function() {},// 报错:function() {} could not be cloned.
        // h: Symbol(1)  // 报错:Symbol(1) could not be cloned.
      }
      const newObj = structuredClone(obj)
      obj.b.n = 3
      console.log(newObj); 
      // 输出{ a: 1, b: { n: 2 }, c: 'cc', d: true, e: undefined, f: null, i: 123n }
    
  • 手写一个深拷贝函数:使用递归机制,逐层复制对象及其属性,确保每一层都是独立的。其他的和浅拷贝差不多

    let obj = {
        a: 1,
        b: {n: 2}
      }
      function deepCopy(obj) {
        let newObj = {}
        for (let key in obj) {
          if (obj.hasOwnProperty(key)) {
            // obj[key] 是不是对象  判断是不是引用类型
            // typeof obj[key] === 'object' && obj[key] !== null
            if (obj[key] instanceof Object) {
              newObj[key] = deepCopy(obj[key])
            } else {
              newObj[key] = obj[key]
            }
          }
        }
        return newObj
      }
    

写在最后

选择浅拷贝还是深拷贝取决于具体需求。浅拷贝快速且节省内存,适用于不需要深度独立性的场景;而深拷贝虽然更全面,但也意味着更高的资源消耗。掌握这些技巧,可以让我们在处理复杂数据结构时更加得心应手。

无论是浅拷贝还是深拷贝,理解它们的工作原理和适用场景都是提升编程技能的关键。希望本文能帮助你更好地掌握JavaScript中的对象拷贝技巧。