JS中的深浅拷贝

141 阅读5分钟

在JavaScript编程中,数据的复制机制是开发者必须掌握的核心概念之一。深浅拷贝作为数据复制的两种主要方式,各自具有独特的特点和应用场景。

一、深浅拷贝的基本概念

浅拷贝:浅拷贝是指创建一个新的对象或数组,但其属性或元素仍然引用原始对象或数组中的相同数据。

  • 如果属性或元素是基本类型(如字符串、数字、布尔值、nullundefinedSymbol以及ES6新增的BigInt),则它们会被复制。因此,修改新对象中的属性,不会影响原始对象中对应的属性。
  • 如果属性或元素是对象或数组(即复合类型),则它们仍然指向原始数据。因此,修改新对象中嵌套的对象或数组的属性会影响原始对象中对应的对象或数组。

深拷贝:深拷贝则是指创建一个新的对象或数组,并递归地复制其所有属性或元素,包括嵌套的对象和数组。这样,新对象和原始对象在内存中是完全独立的,修改新对象不会影响原始对象。

二、浅拷贝的实现方法与例子

  1. 对象字面量/数组字面量

    通过创建一个新的对象或数组,并手动复制属性或元素,可以实现浅拷贝。这种方法适用于结构简单的对象或数组。

    let obj1 = { a: 1, b: { c: 2 } };
    let obj2 = { ...obj1 }; // 使用对象字面量实现浅拷贝
    obj2.b.c = 3;
    console.log(obj1.b.c); // 输出: 3,说明修改了obj2中的嵌套对象,也影响了obj1
    
    let obj1 = { a: 1, b: { c: 2 } };
    let obj2 = { ...obj1 }; // 浅拷贝
    
    obj2.a = 4;
    console.log(obj1.a); // 输出: 1,因为拷贝的a属性是基本数据类型,所以修改了obj2中的a属性,不会影响原始对象obj1中的a属性
    
  2. Object.assign()

    Object.assign()方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。

    let obj1 = { a: 1, b: { c: 2 } };
    let obj2 = Object.assign({}, obj1); // 使用Object.assign()实现浅拷贝
    obj2.b.c = 3;
    console.log(obj1.b.c); // 输出: 3,同样说明修改了obj2中的嵌套对象,也影响了obj1
    
  3. 扩展运算符(...

    扩展运算符可以用于对象或数组的浅拷贝。

    let arr1 = [1, 2, { a: 3 }];
    let arr2 = [...arr1]; // 使用扩展运算符实现数组的浅拷贝
    arr2[2].a = 4;
    console.log(arr1[2].a); // 输出: 4,说明修改了arr2中的嵌套对象,也影响了arr1
    
  4. Array.prototype.slice()Array.prototype.concat()

    对于数组,可以使用slice()concat()方法创建一个数组的浅拷贝。

    let arr1 = [1, 2, { a: 3 }];
    let arr2 = arr1.slice(); // 使用slice()实现数组的浅拷贝
    arr2[2].a = 4;
    console.log(arr1[2].a); // 输出: 4,同样说明修改了arr2中的嵌套对象,也影响了arr1
    let arr3 = arr1.concat(); // 使用concat()实现数组的浅拷贝
    arr3[2].a = 5;
    console.log(arr1[2].a); // 输出: 5,进一步证明了浅拷贝的特性
    

三、深拷贝的实现方法与例子

  1. 递归拷贝

    递归拷贝是一种实现深拷贝的方法,它涉及遍历对象的所有属性,如果属性是对象或数组,则递归调用拷贝函数。

    function deepClone(obj) {
      if (obj === null || typeof obj !== 'object') {
        return obj;
      }
    
      let copy = Array.isArray(obj) ? [] : {};
      for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
          copy[key] = deepClone(obj[key]);
        }
      }
      return copy;
    }
    
    let obj1 = { a: 1, b: { c: 2 } };
    let obj2 = deepClone(obj1); // 使用递归拷贝实现深拷贝
    obj2.b.c = 3;
    console.log(obj1.b.c); // 输出: 2,说明修改了obj2中的嵌套对象,没有影响obj1
    
  2. 使用JSON

    将对象转换为JSON字符串,然后再解析回对象,可以实现深拷贝。但这种方法有其局限性,如无法处理循环引用、函数、undefinedNaNInfinity等特殊值。

    let obj1 = { a: 1, b: { c: 2 } };
    let obj2 = JSON.parse(JSON.stringify(obj1)); // 使用JSON实现深拷贝
    obj2.b.c = 3;
    console.log(obj1.b.c); // 输出: 2,说明JSON方法实现了深拷贝
    

    注意:JSON方法无法处理循环引用和函数等特殊情况。例如,如果对象中包含一个指向自身的属性,则JSON方法会抛出错误。

  3. 使用库

    许多JavaScript库和框架(如Lodash、Ramda、Immutable.js等)提供了深拷贝的功能。这些库通常提供了更可靠、更高效的深拷贝实现,并且处理了上述JSON方法的局限性。

    // 假设已经引入了Lodash库
    let _ = require('lodash');
    
    let obj1 = { a: 1, b: { c: 2 } };
    let obj2 = _.cloneDeep(obj1); // 使用Lodash的cloneDeep方法实现深拷贝
    obj2.b.c = 3;
    console.log(obj1.b.c); // 输出: 2,说明Lodash的cloneDeep方法实现了深拷贝
    

四、深浅拷贝的对比与选择

  • 性能:浅拷贝通常比深拷贝更快,因为它不需要递归地复制嵌套的对象和数组。然而,这种性能差异可能取决于对象的大小和嵌套深度。
  • 复杂性:深拷贝的实现更复杂,因为它需要处理嵌套结构和特殊值。递归拷贝方法需要仔细设计以避免无限递归和栈溢出等问题。
  • 用途:浅拷贝适用于包含简单数据类型(如字符串、数字)的对象或数组,或者当你确定不需要完全独立的副本时。深拷贝适用于需要完全独立副本的复杂数据结构,或者当你需要避免修改原始数据时。