在 JavaScript 开发中,数据拷贝是一个常见且容易出错的操作。浅拷贝(Shallow Copy)与深拷贝(Deep Copy)是两种不同的数据复制方式,它们的核心区别在于如何处理对象的嵌套引用。本文将从原理、实现方法、应用场景及常见问题等方面深入探讨,帮助开发者彻底理解这两个概念。

一、基本概念:值类型与引用类型

1.1 值类型与引用类型的区别

JavaScript 数据类型分为两类:

  • 值类型(Primitive Types):存储的是值本身,包括 stringnumberbooleannullundefinedsymbolbigint
  • 引用类型(Reference Types):存储的是内存地址(引用),包括 ObjectArrayFunctionDateRegExp 等。
// 值类型赋值
let a = 10;
let b = a;
b = 20;
console.log(a); // 10(值类型赋值是值的复制)

// 引用类型赋值
let obj1 = { name: 'Alice' };
let obj2 = obj1;
obj2.name = 'Bob';
console.log(obj1.name); // 'Bob'(引用类型赋值是引用的复制)
1.2 拷贝的本质
  • 浅拷贝:创建一个新对象,新对象的属性值是原对象属性值的引用。如果属性是值类型,拷贝的是值;如果是引用类型,拷贝的是内存地址,因此原对象和拷贝对象会共享引用类型数据。
  • 深拷贝:创建一个新对象,递归复制原对象的所有属性,包括嵌套的引用类型,最终实现完全独立的拷贝。

二、浅拷贝的实现方法

2.1 对象的浅拷贝
  1. 对象字面量展开运算符(...
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { ...obj1 }; // 浅拷贝

obj2.b.c = 3;
console.log(obj1.b.c); // 3(共享引用类型属性)
  1. Object.assign()
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = Object.assign({}, obj1); // 浅拷贝

obj2.b.c = 3;
console.log(obj1.b.c); // 3(同样共享引用)
  1. 手动浅拷贝(遍历属性)
function shallowCopy(obj) {
  const newObj = {};
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      newObj[key] = obj[key]; // 直接复制属性值(引用类型共享地址)
    }
  }
  return newObj;
}
2.2 数组的浅拷贝
  1. 数组展开运算符(...
const arr1 = [1, 2, { a: 3 }];
const arr2 = [...arr1]; // 浅拷贝

arr2[2].a = 4;
console.log(arr1[2].a); // 4(共享引用类型元素)
  1. slice()concat()
const arr1 = [1, 2, { a: 3 }];
const arr2 = arr1.slice(); // 浅拷贝
const arr3 = arr1.concat(); // 浅拷贝

arr2[2].a = 4;
console.log(arr1[2].a); // 4
2.3 浅拷贝的局限性

浅拷贝仅复制第一层属性,对于嵌套的引用类型,原对象和拷贝对象会共享数据,可能导致意外的数据修改。

三、深拷贝的实现方法

3.1 简单场景:JSON.parse(JSON.stringify())
const obj1 = { a: 1, b: { c: 2 }, d: [3, 4] };
const obj2 = JSON.parse(JSON.stringify(obj1)); // 深拷贝

obj2.b.c = 3;
console.log(obj1.b.c); // 2(深拷贝后独立)

局限性

  • 无法处理 functionSymbolDateRegExp 等特殊类型。
  • 会忽略 undefinedSymbol 属性。
  • 无法处理循环引用(会导致报错)。
const obj = { a: 1, b: undefined, c: Symbol('test'), d: new Date() };
const clone = JSON.parse(JSON.stringify(obj));
// clone => { a: 1 }(丢失 undefined、Symbol、Date 会被转换为字符串)
3.2 手动实现深拷贝(递归法)
function deepCopy(obj, visited = new WeakMap()) {
  // 处理 null 和非对象类型
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  // 处理特殊对象(Date/RegExp)
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);

  // 处理循环引用(避免栈溢出)
  if (visited.has(obj)) {
    return visited.get(obj);
  }

  // 创建新对象(数组/普通对象)
  const newObj = Array.isArray(obj) ? [] : {};
  visited.set(obj, newObj); // 记录已复制的对象

  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      newObj[key] = deepCopy(obj[key], visited); // 递归复制
    }
  }

  return newObj;
}

关键点

  1. 处理特殊对象:单独处理 DateRegExp 等内置对象,避免直接创建导致的类型丢失。
  2. 循环引用处理:使用 WeakMap 记录已复制的对象,避免递归时重复复制导致栈溢出。
  3. 数据类型判断:区分数组和普通对象,使用不同的初始化方式。
3.3 处理特殊数据类型
// 测试用例
const obj = {
  a: 1,
  b: undefined,
  c: Symbol('test'),
  d: new Date(2023, 0, 1),
  e: /abc/g,
  f: null,
  g: [1, { h: 2 }],
  i: { j: { k: 3 } },
};

// 使用自定义深拷贝函数
const clone = deepCopy(obj);
console.log(clone.d instanceof Date); // true
console.log(clone.e instanceof RegExp); // true
console.log(clone.g[1].h); // 2(深层属性独立)
3.4 现代浏览器 API:structuredClone

ES10 引入的 structuredClone 方法支持深拷贝几乎所有数据类型,包括函数、Symbol、循环引用等:

const obj = {
  a: 1,
  b: { c: 2 },
  d: function() { console.log('test'); },
  e: Symbol('key'),
};
obj.circular = obj; // 循环引用

const clone = structuredClone(obj);
console.log(clone.d()); // 'test'(函数被复制)
console.log(clone.circular === clone); // true(正确处理循环引用)

四、应用场景与选择策略

4.1 浅拷贝的适用场景
  1. 性能优先的简单数据:当数据结构简单(无嵌套引用类型)或需要快速复制时,使用浅拷贝。
// 快速复制表单数据(假设 fields 是简单对象)
const formData = { name: 'Alice', age: 30 };
const newFormData = { ...formData };
  1. 函数参数安全传递:避免直接修改原始参数,同时不需要深层复制。
function processData(data) {
  const safeData = { ...data }; // 浅拷贝防止原始数据被意外修改
  // 处理数据
}
4.2 深拷贝的适用场景
  1. 状态管理(如 Redux):需要保证状态不可变,避免引用共享导致的副作用。
// Redux reducer 中使用深拷贝
function reducer(state, action) {
  switch (action.type) {
    case 'UPDATE_DATA':
      return { ...state, data: deepCopy(action.payload) };
    default:
      return state;
  }
}
  1. 复杂数据结构克隆:当数据包含多层嵌套的对象或数组时,必须使用深拷贝。
const tree = {
  root: {
    child: {
      grandchild: { value: 42 }
    }
  }
};
const newTree = deepCopy(tree);
newTree.root.child.grandchild.value = 100;
console.log(tree.root.child.grandchild.value); // 42(原始数据不受影响)
  1. 防止第三方库修改原始数据:在使用外部数据时,深拷贝确保数据隔离。
const externalData = api.fetchComplexData();
const localData = deepCopy(externalData); // 避免外部库修改本地数据
4.3 性能考量
  • 浅拷贝:时间复杂度为 O(n),仅复制一层属性,性能较高。
  • 深拷贝:时间复杂度为 O(n^m)(n 为属性数量,m 为嵌套层级),递归复制会带来较高的性能开销,尤其在处理大数据量或深层嵌套时。

建议

  • 优先使用浅拷贝,仅在必要时使用深拷贝。
  • 对性能敏感的场景(如高频操作),避免使用递归深拷贝,可考虑增量更新或Immutable.js 等 Immutable 数据结构。

五、常见误区与解决方案

5.1 误区一:扩展运算符(...)是深拷贝
const arr1 = [1, { a: 2 }];
const arr2 = [...arr1]; // 浅拷贝
arr2[1].a = 3;
console.log(arr1[1].a); // 3(误区:以为修改 arr2 不影响 arr1)

解决方案:明确浅拷贝特性,对嵌套结构使用深拷贝。

5.2 误区二:JSON.parse(JSON.stringify()) 可以处理所有类型
const obj = {
  func: function() {}, // 函数会被忽略
  date: new Date(), // 会被转换为字符串
  sym: Symbol('key'), // 会被忽略
};
const clone = JSON.parse(JSON.stringify(obj));
console.log(clone.func); // undefined
console.log(clone.date); // "2023-10-01T00:00:00.000Z"(字符串而非 Date 对象)

解决方案:使用自定义深拷贝函数或 structuredClone 处理特殊类型。

5.3 误区三:深拷贝一定比浅拷贝好
// 错误示例:对简单数据使用深拷贝导致性能浪费
const simpleObj = { a: 1, b: 2 };
const clone = deepCopy(simpleObj); // 不必要的深拷贝

解决方案:根据数据结构选择拷贝方式,避免过度设计。

六、性能优化与高级技巧

6.1 优化深拷贝性能
  1. 避免不必要的递归:对已知结构简单的数据,使用浅拷贝或混合拷贝。
function hybridCopy(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return obj;
  }
  // 对数组和对象使用浅拷贝,仅对特定属性深拷贝
  return { ...obj, deepProp: deepCopy(obj.deepProp) };
}
  1. 缓存已复制对象:使用 WeakMapMap 避免重复复制同一对象(尤其处理循环引用时)。
function optimizedDeepCopy(obj, cache = new Map()) {
  if (cache.has(obj)) return cache.get(obj);
  // 复制逻辑...
  cache.set(obj, newObj);
  return newObj;
}
6.2 处理循环引用的其他方法
  • 使用 WeakMap:记录已复制的对象,适用于对象作为键的场景。
  • 使用 Map:兼容性更好,但可能导致内存泄漏(需手动清理)。
6.3 第三方库推荐
  • Lodash_.cloneDeep() 提供高效的深拷贝,支持多种数据类型。
import { cloneDeep } from 'lodash';
const deepClone = cloneDeep(obj);
  • Immutable.js:提供持久化数据结构,避免显式拷贝,适合大型应用。

七、面试常见问题

7.1 实现一个深拷贝函数,处理循环引用
function deepClone(obj, visited = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj;
  
  if (visited.has(obj)) {
    return visited.get(obj);
  }
  
  const isArray = Array.isArray(obj);
  const clone = isArray ? [] : {};
  
  visited.set(obj, clone);
  
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] = deepClone(obj[key], visited);
    }
  }
  
  return clone;
}
7.2 浅拷贝和深拷贝的区别是什么?
  • 浅拷贝复制一层属性,引用类型共享内存地址。
  • 深拷贝递归复制所有层级属性,引用类型完全独立。
7.3 如何区分浅拷贝和深拷贝?
  • 修改拷贝对象的属性,观察原对象是否变化:
  • 浅拷贝:原对象的引用类型属性会变化。
  • 深拷贝:原对象不受影响。

八、总结

特性

浅拷贝

深拷贝

拷贝层级

一层(仅值类型值,引用类型地址)

多层(递归复制所有层级)

数据独立性

引用类型共享数据

所有数据独立

性能

高(O(n))

低(O(n^m))

适用场景

简单数据、性能优先

复杂嵌套数据、数据隔离

实现成本

低(内置方法或简单遍历)

高(递归、处理特殊类型)

最佳实践

  1. 优先使用浅拷贝,仅在必要时使用深拷贝。
  2. 对复杂数据结构,使用成熟的深拷贝库(如 Lodash)或 structuredClone
  3. 在状态管理和组件通信中,遵循不可变原则,合理使用拷贝避免副作用。

通过深入理解浅拷贝与深拷贝的原理和适用场景,开发者可以在实际项目中避免数据共享带来的隐患,写出更健壮、高效的代码。