JavaScript对象复制攻略:深拷贝 vs 浅拷贝

118 阅读7分钟

前言:为何需要深浅拷贝?

在 JavaScript 开发中,我们经常需要复制对象或数组,但直接赋值(如 let arr2 = arr1)会导致两个变量指向同一个内存地址,也就是当你修改其中一个时,另一个也会被改变,在很多场景下这是不可接受的。比如:

  • 表单数据的深拷贝(避免修改原始数据)。
  • 配置对象的合并(保留默认配置)。
  • 状态管理(React 中避免直接修改 state)。

于是,深浅拷贝就成为了解决这一问题的关键工具。


一、浅拷贝:表面复制,深层共享

1. 浅拷贝的核心原理

浅拷贝的定义是仅复制对象的第一层属性,具体表现为:

  • 基本类型(如数字、字符串、布尔值) :直接复制值到新对象,修改新对象的属性不会影响原对象。
  • 引用类型(如对象、数组) :复制的是引用地址(内存地址),新对象和原对象共享同一块内存空间。因此,修改嵌套对象的属性会影响原对象。

2. 常见实现方式及特性


2.1 展开运算符(...
const arr1 = [1, 2, 3];
const arr2 = [...arr1]; // 浅拷贝
arr2[1] = 'b'; // 修改第一层属性
console.log(arr1); // [1, 2, 3](原数组未受影响)
console.log(arr2); // [1, "b", 3](新数组已修改)

分析

  • 第一层属性独立arr2 是 arr1 的浅拷贝,修改第一层属性(如 arr2[1])不会影响 arr1
  • 嵌套结构共享:如果 arr1 包含嵌套数组或对象,修改嵌套结构会影响原数组(如 arr1[0][0] = 100 会同步到 arr2[0][0])。

2.2 Object.assign()
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = Object.assign({}, obj1); // 浅拷贝
obj2.b.c = 3; // 修改嵌套属性
console.log(obj1.b.c); // 3(原对象被修改!)

分析

  • 顶层属性独立obj2.a 的修改(如 obj2.a = 10)不会影响 obj1.a
  • 嵌套属性共享obj2.b 是对 obj1.b 的引用,修改 obj2.b.c 会直接修改 obj1.b.c
  • 适用场景:适合复制扁平化对象,但需注意嵌套结构的引用问题。

2.3 slice() / concat()
const arr1 = [[1, 2], [3, 4]];
const arr2 = arr1.slice(); // 浅拷贝
arr2[1][0] = 99; // 修改嵌套数组
console.log(arr1); // [[1, 2], [99, 4]](原数组被修改!)

分析

  • 数组结构独立arr2 是 arr1 的新数组,但嵌套数组的引用被共享。
  • 嵌套修改同步:修改 arr2 中的嵌套数组元素(如 arr2[1][0])会同步到 arr1
  • 适用场景:适合简单数组的浅拷贝,但不适用于多维数组或对象嵌套。

3. 使用浅拷贝的参数前后变化

操作原对象(arr1 / obj1新对象(arr2 / obj2
修改第一层属性(如 a无变化新对象属性更新
修改嵌套属性(如 b.c被同步修改新对象属性更新
替换嵌套对象(如 b = {}无变化新对象属性独立

4. 适用场景与注意事项

适用场景
  1. 简单数据结构

    • 数据层级单一,无需递归复制(如 { a: 1, b: 2 })。
    • 示例:React 中的组件状态更新(通过浅拷贝避免直接修改原状态)。
  2. 性能敏感场景

    • 大型对象或数组的快速复制(如 Redux 的状态合并)。
    • 浅拷贝速度比深拷贝快,内存占用更低。
注意事项
  1. 嵌套结构风险

    • 修改嵌套对象属性会导致原对象同步变化,需谨慎处理。
    • 示例:obj2.b.c = 3 会修改 obj1.b.c
  2. 替代方案

    • 对于嵌套结构需完全独立,应使用深拷贝(如 JSON.parse(JSON.stringify()) 或第三方库 lodash.cloneDeep)。
  3. 循环引用问题

    • 浅拷贝无法处理对象间的循环引用(如 obj1.self = obj1),可能导致栈溢出。

二、深拷贝:彻底复制,独立内存

1. 深拷贝的核心原理

深拷贝是指递归复制对象的所有层级属性,包括嵌套对象和数组。其特点如下:

  • 完全独立:新对象与原对象在内存中互不干扰,修改新对象的任何属性都不会影响原对象。
  • 递归处理:对嵌套结构(如对象、数组)进行逐层复制,直到所有层级均为基本类型或原始引用类型。

2. 常见实现方式及特性分析


2.1 JSON 序列化与反序列化
const obj1 = { a: 1, b: { c: 2 }, d: new Date(), e: undefined };
const obj2 = JSON.parse(JSON.stringify(obj1));
obj2.b.c = 3;
console.log(obj1.b.c); // 2(原对象未被修改)

功能特性

  • 优点

    • 代码简洁,适合纯数据对象的深拷贝。
    • 性能较好(无需递归函数)。
  • 缺点

    • 不支持非 JSON 兼容类型:函数、undefinedSymbol 等会被忽略。
    • 特殊对象丢失Date 对象会被转为字符串,RegExpMapSet 等会被忽略。
    • 循环引用报错:如 obj.self = obj 会导致 TypeError

2.2 递归拷贝(手动实现)
function deepClone(source) {
  if (typeof source !== 'object' || source === null) return source;
  const target = Array.isArray(source) ? [] : {};
  for (const key in source) {
    if (Object.prototype.hasOwnProperty.call(source, key)) {
      target[key] = deepClone(source[key]);
    }
  }
  return target;
}

const obj1 = { a: 1, b: { c: 2 } };
const obj2 = deepClone(obj1);
obj2.b.c = 3;
console.log(obj1.b.c); // 2(原对象未被修改)

功能特性

  • 优点

    • 支持嵌套对象和数组的递归复制。
    • 可扩展性强(可添加对特殊对象的处理逻辑)。
  • 缺点

    • 无法处理循环引用:如 obj.self = obj 会导致栈溢出。
    • 特殊对象需额外处理:如 DateRegExpMapSet 等需要单独判断。
    • 内存开销较大:递归深度过大会导致性能下降。

2.3 第三方库(如 Lodash 的 _.cloneDeep
const _ = require('lodash');
const obj1 = { a: 1, b: { c: 2 }, d: new Date(), e: new Set([1, 2]) };
const obj2 = _.cloneDeep(obj1);
obj2.b.c = 3;
console.log(obj1.b.c); // 2(原对象未被修改)

功能特性

  • 优点

    • 功能全面:支持复杂数据类型(如 DateRegExpMapSet)。
    • 处理循环引用:通过记录已访问对象避免栈溢出。
    • 兼容性好:经过广泛测试,适用于生产环境。
  • 缺点

    • 依赖外部库:需引入 Lodash,可能增加项目体积。
    • 性能略低:相比手动实现的递归拷贝,复杂度稍高。

3. 使用深拷贝的参数前后变化

操作原对象(obj1新对象(obj2
修改第一层属性(如 a无变化新对象属性更新
修改嵌套属性(如 b.c无变化新对象属性更新
替换嵌套对象(如 b = {}无变化新对象属性独立

4. 适用场景与注意事项

适用场景
  1. 复杂嵌套结构

    • 需要完全隔离新旧对象(如配置对象、状态管理)。
    • 示例:React 中的组件状态更新需避免引用共享。
  2. 特殊数据类型

    • 包含 DateMapSet 等对象时,推荐使用第三方库。
  3. 数据安全性要求高

    • 修改新对象不会影响原对象,适合敏感数据处理。
注意事项
  1. 避免循环引用

    • 递归实现需添加循环检测逻辑(如 WeakMap 记录已访问对象)。
  2. 性能权衡

    • 大型对象的深拷贝可能导致性能问题,需根据场景选择实现方式。
  3. 特殊对象处理

    • 如需保留 Date 实例或 Map 结构,需手动处理或依赖第三方库。

三、深浅拷贝的选择策略

1. 根据数据结构选择

  • 浅拷贝:适用于简单数据结构或仅需修改第一层属性的场景。
  • 深拷贝:适用于嵌套结构或需要完全独立的副本时。

2. 根据性能选择

  • 浅拷贝:速度快,适合大型对象。
  • 深拷贝:速度慢,但能保证数据独立性。

3. 根据数据类型选择

  • JSON 方法:仅适合纯数据对象(无函数、Symbol)。
  • 递归/第三方库:适合复杂数据类型(如嵌套对象、特殊对象)。

四、实际应用场景

1. React 中的状态管理

在 React 函数组件中,直接修改 state 会导致渲染问题。通过深拷贝更新状态,可避免副作用:

const [user, setUser] = useState({ name: "Alice", settings: { theme: "dark" } });

// 错误:直接修改 state
user.settings.theme = "light"; // ❌ 不会触发重渲染

// 正确:深拷贝后更新
setUser(JSON.parse(JSON.stringify(user)));
user.settings.theme = "light";

2. 表单数据的初始化

在表单组件中,深拷贝原始数据可避免用户输入污染默认值:

const defaultData = { name: "", age: 18 };
const [formData, setFormData] = useState(JSON.parse(JSON.stringify(defaultData)));

3. 配置对象的合并

合并用户配置与默认配置时,浅拷贝可能导致嵌套配置被覆盖:

const defaults = { timeout: 500, api: "/api" };
const userConfig = { timeout: 1000, debug: true };
const config = Object.assign({}, defaults, userConfig); // ❌ 嵌套配置未合并