深入理解JS(十):一文吃透浅拷贝与深拷贝

142 阅读13分钟

前言

在日常的 JavaScript 开发工作中,我们经常会遇到需要复制对象或数组的情况。比如,你想要修改一个用户信息对象,但又不想影响到原始数据;或者你需要基于现有的配置创建一个新的配置对象。这时候,数据拷贝就显得非常重要了。
本文将帮你彻底理解浅拷贝和深拷贝的概念、原理、实现方法和应用场景,让你在实际开发中能够游刃有余地处理各种数据拷贝需求。

一、前置:JS 的数据类型与赋值操作

(一)JS数据类型与内存存储

要理解拷贝的本质,我们首先需要搞清楚 JavaScript 中数据是如何存储的。我在《深入理解JS(二) - 堆栈与数据类型》这篇文章中详细介绍过,这里简要回顾一下。JavaScript 的数据类型可以分为两大类:

  • 基本数据类型(原始类型)

    • Number(数字):如 42, 3.14
    • String(字符串):如 "hello", 'world'
    • Boolean(布尔值):truefalse
    • Null:表示空值
    • Undefined:表示未定义
    • Symbol:ES6 新增的唯一标识符
    • BigInt:ES2020 新增的大整数类型
  • 引用数据类型(复杂类型)

    • Object(对象):如 { name: 'Alice' }
    • Array(数组):如 [1, 2, 3]
    • Function(函数):如 function() {}
    • Date(日期):如 new Date()
    • RegExp(正则表达式):如 /test/g
    • 以及其他内置对象类型
  • 关键区别在于存储方式

    • 基本数据类型直接存储在栈内存中。想象栈内存就像一个书架,每个变量就是书架上的一个格子,格子里直接放着数据的值。当你复制一个基本类型的变量时,就相当于把这个值完整地复制到另一个格子里,两个格子里的内容是完全独立的。

    • 引用数据类型则存储在堆内存中,而变量中保存的只是一个"地址"(引用)。想象堆内存就像一个大仓库,对象存放在仓库里,而变量中保存的是这个仓库的地址。当你复制一个引用类型的变量时,复制的只是这个地址,而不是仓库里的实际内容。这就是为什么修改复制后的对象会影响原对象的根本原因。

(二)JS赋值操作的缺陷

为了理解为什么需要拷贝,我们先来看看简单的赋值操作是如何工作的,然后我们就能明白它为什么不能满足我们的需求了。

  • 基本数据类型

    // 基本数据类型的赋值
    let a = 10;        // a 指向栈内存中存储值 10 的位置
    let b = a;         // b 获得了 a 的值的副本,也是 10
    b = 20;            // 修改 b 的值为 20
    console.log(a);    // 输出 10,a 不受影响
    console.log(b);    // 输出 20
    
    // 这里发生了什么?
    // 1. a 和 b 是两个完全独立的变量
    // 2. 它们各自在栈内存中有自己的存储空间
    // 3. 修改其中一个不会影响另一个
    
  • 引用数据类型

    // 引用数据类型的赋值
    let obj1 = { name: 'Alice', age: 25 };  // obj1 保存指向堆内存中对象的地址
    let obj2 = obj1;                        // obj2 获得了 obj1 的地址副本
    obj2.name = 'Bob';                      // 通过 obj2 修改对象的属性
    console.log(obj1.name);                 // 输出 'Bob',obj1 受到了影响!
    console.log(obj2.name);                 // 输出 'Bob'
    
    // 这里发生了什么?
    // 1. obj1 和 obj2 保存的是同一个内存地址
    // 2. 它们指向堆内存中的同一个对象
    // 3. 通过任何一个变量修改对象,都会影响到另一个变量
    // 4. 因为它们实际上操作的是同一个对象
    

这个例子清楚地展示了为什么我们需要拷贝:当我们想要一个对象的独立副本时,简单的赋值是不够的。

二、浅拷贝(Shallow Copy)

(一)什么是浅拷贝?

浅拷贝就像是给一本书做目录复印。你复印了目录页,但是目录中提到的具体章节内容还是指向原书的页面

具体来说,浅拷贝会创建一个新的对象,并将原对象的所有属性复制到新对象中。但是:

  • 如果属性值是基本数据类型,会复制实际的值
  • 如果属性值是引用数据类型,只会复制引用地址

这意味着浅拷贝只解决了第一层的独立性问题(基本数据类型完全独立,引用数据类型不独立),对于嵌套的对象或数组,仍然存在共享引用的问题

(二)浅拷贝的特点

  • 对象的层级结构:为了更好地理解浅拷贝的特点,我们先来通过对象的层级结构,明确什么是 "第一层"和"嵌套层"

    const user = {
      // 第一层属性(直接属于 user 对象的属性)
      name: 'Alice',        // 基本类型
      age: 25,              // 基本类型
    
      // 第一层属性,但值是引用类型
      hobbies: ['reading', 'swimming'],  // 数组(引用类型)
    
      // 第一层属性,但值是对象(引用类型)
      address: {            // 这是第一层
        // 第二层属性(嵌套在 address 对象内部的属性)
        city: 'Beijing',    // 这是第二层
        district: 'Chaoyang', // 这是第二层
    
        // 第二层属性,但值又是对象
        coordinates: {      // 这是第二层
          // 第三层属性(嵌套在 coordinates 对象内部)
          lat: 39.9042,     // 这是第三层
          lng: 116.4074     // 这是第三层
        }
      }
    };
    
  • 特点:

    • 1. 第一层基本类型属性完全独立

      • nameage 这样直接属于对象的基本类型属性
      • 修改拷贝对象的这些属性不会影响原对象
    • 2. 第一层引用类型属性共享引用

      • hobbiesaddress 这样的第一层属性会被复制
      • 但它们指向的数组或对象内容是共享的
      • 修改 address.cityhobbies[0] 会影响原对象
    • 3. 性能较好:只需要遍历对象的直接属性,不需要递归处理嵌套内容

    • 4. 内存占用少:嵌套的对象和数组不会重复创建,节省内存空间

(三)浅拷贝的实现方法

1. Object.assign() 方法

Object.assign() 是 ES6 提供的官方方法,用于将一个或多个源对象的属性复制到目标对象。

const original = {
  name: 'Alice',           // 基本类型
  age: 25,                 // 基本类型
  hobbies: ['reading', 'swimming'],  // 引用类型(数组)
  address: {               // 引用类型(对象)
    city: 'Beijing',
    district: 'Chaoyang'
  }
};

// 使用 Object.assign() 进行浅拷贝
const shallowCopy = Object.assign({}, original);

// 测试第一层属性的独立性
shallowCopy.name = 'Bob';
shallowCopy.age = 30;
console.log('原对象 name:', original.name);     // 'Alice' - 不受影响
console.log('拷贝对象 name:', shallowCopy.name); // 'Bob'
console.log('原对象 age:', original.age);       // 25 - 不受影响
console.log('拷贝对象 age:', shallowCopy.age);   // 30

// 测试嵌套对象的共享性
shallowCopy.address.city = 'Shanghai';
console.log('原对象 city:', original.address.city);     // 'Shanghai' - 受到影响!
console.log('拷贝对象 city:', shallowCopy.address.city); // 'Shanghai'

// 测试嵌套数组的共享性
shallowCopy.hobbies.push('coding');
console.log('原对象 hobbies:', original.hobbies);       // ['reading', 'swimming', 'coding'] - 受到影响!
console.log('拷贝对象 hobbies:', shallowCopy.hobbies);   // ['reading', 'swimming', 'coding']
  • 为什么会这样?
    • nameage 是基本类型,Object.assign() 复制了它们的实际值
    • addresshobbies 是引用类型,Object.assign() 只复制了它们的引用地址
    • 所以 original.addressshallowCopy.address 指向同一个对象
    • 修改其中一个会影响另一个

2. 扩展运算符(Spread Operator)

扩展运算符 ... 是 ES6 引入的语法,提供了一种更简洁的浅拷贝方式。

const original = {
  name: 'Alice',
  skills: ['JavaScript', 'Python'],
  info: { age: 25, city: 'Beijing' }
};

// 使用扩展运算符进行浅拷贝
const shallowCopy = { ...original };

// 效果与 Object.assign() 完全相同
shallowCopy.name = 'Bob';
console.log(original.name);        // 'Alice' - 不受影响

shallowCopy.info.age = 30;
console.log(original.info.age);    // 30 - 受到影响!

// 扩展运算符的优势:语法更简洁,可读性更好
// 还可以在拷贝的同时添加或覆盖属性
const enhancedCopy = {
  ...original,
  name: 'Charlie',      // 覆盖原有属性
  email: 'charlie@example.com'  // 添加新属性
};

3. 针对数组的浅拷贝方法

数组作为特殊的对象,有自己专门的浅拷贝方法:

const originalArray = [1, 2, [3, 4], { name: 'Alice' }];

// 方法1:Array.from()
const shallowCopy1 = Array.from(originalArray);

// 方法2:slice() 方法
const shallowCopy2 = originalArray.slice();

// 方法3:扩展运算符
const shallowCopy3 = [...originalArray];

// 方法4:concat() 方法
const shallowCopy4 = [].concat(originalArray);

// 测试效果(所有方法效果相同)
console.log('原数组:', originalArray);           // [1, 2, [3, 4], { name: 'Alice' }]
console.log('拷贝数组:', shallowCopy1);          // [1, 2, [3, 4], { name: 'Alice' }]

// 修改第一层元素
shallowCopy1[0] = 999;
console.log('原数组第一个元素:', originalArray[0]);  // 1 - 不受影响
console.log('拷贝数组第一个元素:', shallowCopy1[0]); // 999

// 修改嵌套数组
shallowCopy1[2].push(5);
console.log('原数组嵌套数组:', originalArray[2]);   // [3, 4, 5] - 受到影响!
console.log('拷贝数组嵌套数组:', shallowCopy1[2]);  // [3, 4, 5]

// 修改嵌套对象
shallowCopy1[3].name = 'Bob';
console.log('原数组嵌套对象:', originalArray[3].name);  // 'Bob' - 受到影响!
console.log('拷贝数组嵌套对象:', shallowCopy1[3].name); // 'Bob'

(四)手写浅拷贝函数

理解原理后,我们可以手动实现一个浅拷贝函数:

function shallowCopy(obj) {
  // 处理非对象类型(基本类型、null、函数等)
  if (typeof obj !== 'object' || obj === null) {
    return obj;
  }
  
  // 处理数组
  if (Array.isArray(obj)) {
    const arrCopy = [];
    for (let i = 0; i < obj.length; i++) {
      arrCopy[i] = obj[i];  // 只复制第一层
    }
    return arrCopy;
    // 或者简单地使用:return [...obj];
  }
  
  // 处理普通对象
  const objCopy = {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {  // 只复制自有属性,不包括继承的属性
      objCopy[key] = obj[key];      // 只复制第一层
    }
  }
  return objCopy;
}

// 测试我们的浅拷贝函数
const original = {
  name: 'Alice',
  hobbies: ['reading'],
  address: { city: 'Beijing' }
};

const copied = shallowCopy(original);
copied.name = 'Bob';                    // 不影响原对象
copied.address.city = 'Shanghai';       // 影响原对象

console.log(original.name);             // 'Alice'
console.log(original.address.city);     // 'Shanghai' - 被影响了

(五)浅拷贝的应用场景

浅拷贝在以下场景中非常有用:

1. React 状态管理

在 React 中,状态更新需要创建新的对象来触发重新渲染:

// React 函数组件示例
function UserProfile() {
  const [user, setUser] = useState({
    name: 'Alice',
    email: 'alice@example.com',
    preferences: {
      theme: 'dark',
      language: 'en'
    }
  });

  // 错误的做法:直接修改状态
  const updateNameWrong = (newName) => {
    user.name = newName;  // 这样不会触发重新渲染
    setUser(user);
  };

  // 正确的做法:使用浅拷贝创建新对象
  const updateName = (newName) => {
    setUser({
      ...user,           // 浅拷贝原有属性
      name: newName      // 覆盖特定属性
    });
  };

  // 更新嵌套对象时需要特别注意
  const updateTheme = (newTheme) => {
    setUser({
      ...user,
      preferences: {
        ...user.preferences,  // 也需要浅拷贝嵌套对象
        theme: newTheme
      }
    });
  };
}
  • 注意:使用扩展运算符时,应该将其放在前面,使得后面修改的属性值可以覆盖原有的属性值。如果后面修改的属性值放在前面,则会被原有属性值覆盖。

2. 数组操作

避免直接修改原数组,保持数据的不可变性:

const originalList = [
  { id: 1, name: 'Apple', price: 1.2 },
  { id: 2, name: 'Banana', price: 0.8 },
  { id: 3, name: 'Orange', price: 1.5 }
];

// 添加新项目(不修改原数组)
const addItem = (list, newItem) => {
  return [...list, newItem];  // 浅拷贝并添加
};

// 删除项目(不修改原数组)
const removeItem = (list, itemId) => {
  return list.filter(item => item.id !== itemId);  // filter 返回新数组
};

// 更新项目(不修改原数组)
const updateItem = (list, itemId, updates) => {
  return list.map(item => 
    item.id === itemId 
      ? { ...item, ...updates }  // 浅拷贝并更新
      : item
  );
};

// 使用示例
const newList = addItem(originalList, { id: 4, name: 'Grape', price: 2.0 });
console.log('原列表长度:', originalList.length);  // 3 - 不变
console.log('新列表长度:', newList.length);       // 4

三、深拷贝(Deep Copy)

(一)什么是深拷贝?

如果说浅拷贝像是复印书的目录,那么深拷贝就像是把整本书完整地复印一遍,包括所有的章节内容

深拷贝会递归地复制对象的所有层级,创建一个完全独立的副本。无论原对象的结构多么复杂,有多少层嵌套,深拷贝都会为每一层创建新的对象或数组。这样,修改拷贝后的对象的任何部分都不会影响原对象。

(二)深拷贝的特点

    1. 完全独立:拷贝对象与原对象完全独立,互不影响
    1. 递归复制:会递归地复制所有嵌套的对象和数组
    1. 内存占用大:会创建所有嵌套对象的副本,占用更多内存
    1. 性能开销大:需要遍历所有层级,执行时间较长
    1. 处理复杂:需要考虑循环引用、特殊对象类型等问题

(三)深拷贝的实现方法

1. JSON.parse(JSON.stringify()) 方法

这是最简单也是最常用的深拷贝方法,利用 JSON 的序列化和反序列化来实现。

  • JSON 方法的工作原理

      1. JSON.stringify(original) 将对象转换为 JSON 字符串
      1. JSON.parse() 将 JSON 字符串解析回对象
      1. 由于 JSON 不支持引用,所以得到的是完全独立的对象
  • 示例

    const original = {
      name: 'Alice',
      age: 25,
      hobbies: ['reading', 'swimming'],
      address: {
        city: 'Beijing',
        district: 'Chaoyang',
        coordinates: {
          latitude: 39.9042,
          longitude: 116.4074
        }
      }
    };
    
    // 使用 JSON 方法进行深拷贝
    const deepCopy = JSON.parse(JSON.stringify(original));
    
    // 测试完全独立性
    deepCopy.name = 'Bob';
    deepCopy.address.city = 'Shanghai';
    deepCopy.address.coordinates.latitude = 31.2304;
    deepCopy.hobbies.push('coding');
    
    console.log('原对象 name:', original.name);                           // 'Alice' - 不受影响
    console.log('拷贝对象 name:', deepCopy.name);                         // 'Bob'
    
    console.log('原对象 city:', original.address.city);                   // 'Beijing' - 不受影响
    console.log('拷贝对象 city:', deepCopy.address.city);                 // 'Shanghai'
    
    console.log('原对象 latitude:', original.address.coordinates.latitude); // 39.9042 - 不受影响
    console.log('拷贝对象 latitude:', deepCopy.address.coordinates.latitude); // 31.2304
    
    console.log('原对象 hobbies:', original.hobbies);                     // ['reading', 'swimming'] - 不受影响
    console.log('拷贝对象 hobbies:', deepCopy.hobbies);                   // ['reading', 'swimming', 'coding']
    
  • JSON 方法的局限性

虽然 JSON 方法简单易用,但它有很多局限性,主要包括:

  • 1. 不支持的数据类型

    • 函数:会被完全丢失
    • undefined:会被忽略
    • Symbol:会被忽略
    • NaN 和 Infinity:会被转换为 null
  • 2. 特殊对象类型处理不当

    • Date 对象:会被转换为字符串,失去 Date 类型
    • RegExp 对象:会被转换为空对象 {}
    • Map 和 Set:会被转换为空对象 {}
  • 3. 循环引用问题

    • 当对象存在循环引用时,会抛出错误
  • 示例:让我们通过代码来验证这些局限性。

    • 数据类型不支持或特殊对象处理不当

      // 测试各种数据类型的处理
      const complexObj = {
        // 支持的类型
        name: 'Alice',
        age: 25,
      
        // 不支持的类型
        greet: function() { return `Hello, I'm ${this.name}`; },  // 函数
        undefinedValue: undefined,                                // undefined
        symbolKey: Symbol('test'),                               // Symbol
        notANumber: NaN,                                         // NaN
        infinity: Infinity,                                      // Infinity
      
        // 特殊对象类型
        birthday: new Date('1998-01-01'),                        // Date
        pattern: /test/g,                                        // RegExp
        map: new Map([['key1', 'value1']]),                     // Map
        set: new Set([1, 2, 3])                                 // Set
      };
      
      const jsonCopy = JSON.parse(JSON.stringify(complexObj));
      
      console.log('原对象类型检查:');
      console.log('函数存在:', typeof complexObj.greet === 'function');        // true
      console.log('Date类型:', complexObj.birthday instanceof Date);           // true
      console.log('RegExp类型:', complexObj.pattern instanceof RegExp);        // true
      
      console.log('\nJSON拷贝后的变化:');
      console.log('函数丢失:', jsonCopy.greet);                               // undefined
      console.log('Date变字符串:', typeof jsonCopy.birthday);                  // "string"
      console.log('RegExp变空对象:', JSON.stringify(jsonCopy.pattern));        // "{}"
      console.log('NaN变null:', jsonCopy.notANumber);                        // null
      
    • 循环引用测试

      const obj = { name: 'test' };
      obj.self = obj;  // 创建循环引用
      
      try {
        const copy = JSON.parse(JSON.stringify(obj));
      } catch (error) {
        console.error('JSON方法无法处理循环引用:', error.message);
        // 输出:Converting circular structure to JSON
      }
      

2. 手写深拷贝函数

为了解决 JSON 方法的局限性,我们可以手写一个深拷贝函数。

  • 核心要点:手写的深拷贝函数需要符合以下几个关键点。

    • 递归处理:对每一层的对象和数组都进行递归拷贝
    • 循环引用处理:使用 WeakMap 记录已拷贝的对象,避免无限递归
    • 特殊类型处理:正确处理 Date、RegExp、Map、Set 等特殊对象
    • 类型判断:准确识别不同的数据类型并采用相应的拷贝策略
  • 示例代码

    /**
     * 深拷贝函数 - 递归复制对象的所有层级
     * @param {*} obj - 需要拷贝的对象
     * @param {WeakMap} hash - 用于记录已拷贝对象的映射表,防止循环引用
     * @returns {*} 深拷贝后的新对象
     */
    function deepClone(obj, hash = new WeakMap()) {
      // 第一步:处理基本类型和 null
      // 基本类型(number、string、boolean等)直接返回,无需拷贝
      if (obj === null || typeof obj !== 'object') return obj;
    
      // 第二步:检查循环引用
      // 如果当前对象已经被拷贝过,直接返回之前的拷贝结果
      // 这样可以避免 A->B->A 这种循环引用导致的无限递归
      if (hash.has(obj)) return hash.get(obj);
    
      // 第三步:根据构造函数判断对象类型
      // 通过 constructor 属性可以准确识别对象的具体类型
      const Constructor = obj.constructor;
    
      // 第四步:根据不同类型采用相应的拷贝策略
      switch (Constructor) {
        case Date:
          // Date 对象:创建新的 Date 实例,保持相同的时间值
          return new Date(obj);
    
        case RegExp:
          // 正则表达式:创建新的 RegExp 实例,保持相同的模式和标志
          return new RegExp(obj);
    
        case Map:
          // Map 对象处理
          const mapCopy = new Map();
          // 先将空的 Map 存入 hash,防止循环引用
          hash.set(obj, mapCopy);
          // 遍历原 Map,递归拷贝每个值(键通常是基本类型,不需要拷贝)
          obj.forEach((value, key) => {
            mapCopy.set(key, deepClone(value, hash));
          });
          return mapCopy;
    
        case Set:
          // Set 对象处理
          const setCopy = new Set();
          // 先将空的 Set 存入 hash,防止循环引用
          hash.set(obj, setCopy);
          // 遍历原 Set,递归拷贝每个值
          obj.forEach(value => {
            setCopy.add(deepClone(value, hash));
          });
          return setCopy;
    
        case Array:
          // 数组处理
          const arrCopy = [];
          // 先将空数组存入 hash,防止循环引用
          hash.set(obj, arrCopy);
          // 遍历原数组,递归拷贝每个元素
          obj.forEach((item, index) => {
            arrCopy[index] = deepClone(item, hash);
          });
          return arrCopy;
    
        default:
          // 普通对象处理(包括自定义对象、函数等)
          const objCopy = {};
          // 先将空对象存入 hash,防止循环引用
          hash.set(obj, objCopy);
          // 遍历对象的所有可枚举属性,递归拷贝每个属性值
          Object.keys(obj).forEach(key => {
            objCopy[key] = deepClone(obj[key], hash);
          });
          return objCopy;
      }
    }
    
  • 测试验证

    // 测试数据
    const testObj = {
      name: 'Alice',
      date: new Date('2025-08-11'),
      regex: /test/gi,
      map: new Map([['key1', 'value1']]),
      set: new Set([1, 2, 3]),
      nested: {
        array: [1, { deep: 'value' }],
        level: 2
      }
    };
    
    // 添加循环引用
    testObj.self = testObj;
    
    const cloned = deepClone(testObj);
    
    // 验证独立性
    cloned.name = 'Bob';
    cloned.nested.level = 3;
    cloned.map.set('key2', 'value2');
    
    console.log('原对象 name:', testObj.name);           // 'Alice'
    console.log('拷贝对象 name:', cloned.name);          // 'Bob'
    console.log('原对象 level:', testObj.nested.level);  // 2
    console.log('拷贝对象 level:', cloned.nested.level); // 3
    console.log('循环引用处理:', cloned.self === cloned); // true
    

3. 使用第三方库

在生产环境中,推荐使用经过充分测试的第三方库,处理深拷贝最常用的第三方库是 Lodash,其中的 _.cloneDeep() 方法是深拷贝场景下的行业标准之一,被广泛应用于各类项目中。

  • Lodash的_.cloneDeep()

    const _ = require('lodash');
    
    const original = {
      user: { name: 'Alice', settings: { theme: 'dark' } },
      data: [1, 2, { nested: true }],
      date: new Date(),
      regex: /pattern/g
    };
    
    const cloned = _.cloneDeep(original);
    
    // 修改不会影响原对象
    cloned.user.name = 'Bob';
    cloned.data[2].nested = false;
    
    console.log(original.user.name);     // 'Alice'
    console.log(cloned.user.name);       // 'Bob'
    

(四)深拷贝的应用场景

深拷贝在以下场景中是必需的,这些场景要求对象的完全独立性:

1. 保存数据快照(如表单回滚、历史记录)

当需要保存数据在某一时刻的"快照",以便后续对比、回滚或查看历史时,必须使用深拷贝。

  • 典型场景:用户填写复杂表单(含嵌套字段,如 { user: { name: "", address: { city: "" } } })时,需要在提交前保存原始数据。如果用浅拷贝,修改表单时原始快照会被同步修改,导致无法回滚到初始状态。

    // 表单管理器示例
    class FormManager {
      constructor(initialData) {
        this.currentData = initialData;
        this.originalSnapshot = deepClone(initialData);  // 深拷贝保存原始快照
      }
    
      rollback() {
        this.currentData = deepClone(this.originalSnapshot);
        return this.currentData;
      }
    }
    

深拷贝能确保快照数据与当前操作的数据完全隔离,实现真正的数据回滚功能。

2. 状态管理(如框架中的不可变状态)

在 React、Vue 等框架中,状态通常被设计为"不可变"——不直接修改原状态,而是创建新状态替换旧状态。

  • 核心问题:修改嵌套的状态数据 state = { list: [{ id: 1, info: { name: "a" } }] } 时,若用浅拷贝复制 list,修改其中 info.name 会同时影响原状态,导致框架无法检测到状态变化(因为引用地址未变)。

    // 错误:浅拷贝无法满足不可变原则
    const newState = { ...state };  // 浅拷贝
    newState.list[0].info.name = 'new name';  // 影响了原状态!
    
    // 正确:深拷贝确保完全不可变
    const newState = deepClone(state);
    newState.list[0].info.name = 'new name';  // 完全独立
    

深拷贝能创建全新的嵌套对象,确保状态更新符合"不可变"原则,触发正确的重新渲染。

3. 处理可复用的配置对象

当需要基于一个"基础配置"创建多个变体配置时,深拷贝可以避免变体间的相互影响。

  • 典型场景:有一个基础主题配置 baseTheme = { color: "red", font: { size: 14 } },需要创建 darkThemelightTheme 两个变体。若用浅拷贝,修改 darkTheme.font.size 会同步修改 baseThemefont.size,导致基础配置被污染。

    // 正确做法:深拷贝确保配置独立
    function createTheme(baseTheme, overrides) {
      const theme = deepClone(baseTheme);  // 深拷贝
      return Object.assign(theme, overrides);
    }
    
    const darkTheme = createTheme(baseTheme, { color: 'black' });
    const lightTheme = createTheme(baseTheme, { color: 'white' });
    

深拷贝能让每个变体配置都是独立的副本,安全地修改各自的嵌套属性。

4. 避免函数参数的意外修改

当函数接收一个对象作为参数,且需要在函数内部修改该对象时,深拷贝可以防止修改影响函数外部的原对象。

  • 典型场景:函数 formatData(data) 需要对传入的 data(含嵌套数组 data.items = [{...}])进行格式化处理。若直接操作原对象,函数外部的 data 会被同步修改,可能导致不可预期的副作用。

    function formatData(data) {
      const formattedData = deepClone(data);  // 深拷贝防止修改原数据
      // 安全地进行各种格式化操作
      formattedData.items.forEach(item => {
        item.price = ${item.price.toFixed(2)}`;
      });
      return formattedData;
    }
    

深拷贝后再处理,能保证原数据的完整性,避免函数的副作用。

5. 复杂数据的序列化/持久化

在将对象转换为字符串(如 JSON.stringify)进行存储(如 localStorage)或传输时,若对象中存在循环引用或特殊类型(如 DateRegExp),原生序列化可能丢失信息,此时常需结合深拷贝进行预处理。

  • 核心价值:深拷贝时可将 Date 对象转换为字符串保存,还原时再解析,确保数据序列化后能正确恢复。

    // 序列化预处理
    function prepareForSerialization(obj) {
      return deepClone(obj, (key, value) => {
        if (value instanceof Date) {
          return { __type: 'Date', value: value.toISOString() };
        }
        return value;
      });
    }
    

通过深拷贝结合自定义处理逻辑,可以安全地序列化和恢复复杂数据结构。

四、性能差异

JS 中浅拷贝和深拷贝的性能差异主要源于处理数据的范围不同,核心区别如下:

  • 浅拷贝仅复制对象表层属性,对嵌套的引用类型(如对象、数组)只复制引用地址。因此操作简单,无需递归,时间开销极小(时间复杂度为表层属性数量的 O (n)),空间开销也很小(仅新增表层对象,嵌套数据共享内存)。

  • 深拷贝需递归遍历所有嵌套层级,为每个引用类型创建全新副本。因此时间开销大(时间复杂度为所有层级总属性数的 O (n),嵌套越深开销越大),空间开销也高(需为所有嵌套数据分配新内存,可能接近原对象总大小),且需处理循环引用、特殊类型等边缘情况,进一步增加性能成本。

简言之:浅拷贝因操作范围有限而高效,深拷贝因全量递归处理而开销大,数据越复杂(嵌套深、规模大),两者性能差距越明显。

结语

浅拷贝和深拷贝是 JavaScript 开发中的基础概念,但它们的重要性往往被低估。正确理解和使用这两种拷贝方式,不仅能帮助我们避免许多隐蔽的 bug,还能在性能和内存使用上做出更明智的选择。掌握了浅拷贝和深拷贝的原理和应用,你就拥有了处理复杂数据操作的强大工具。

希望这篇文章有帮助到你,如果文章有错误,请你在评论区指出,大家一起进步,谢谢🙏。