Skip to content

浅拷贝vs深拷贝

1. 概述

在JavaScript中,对象和数组的复制是一个常见而复杂的话题。浅拷贝和深拷贝是两种主要的复制方法,理解它们的区别和使用场景对于编写高质量的代码至关重要。本文将深入探讨这两种拷贝方法的原理、实现和应用,并提供面试中常见的相关问题及其解答。

2. 数据类型与内存分配

在深入浅拷贝和深拷贝之前,我们需要理解JavaScript中的数据类型和内存分配机制。

2.1 基本类型和引用类型

JavaScript中的数据类型可以分为两类:

  1. 基本类型:Number, String, Boolean, null, undefined, Symbol, BigInt
  2. 引用类型:Object, Array, Function, Date, RegExp等

2.2 内存分配

  • 基本类型:存储在栈(Stack)中
  • 引用类型:存储在堆(Heap)中,栈中存储的是指向堆中实际对象的引用

3. 浅拷贝

3.1 定义

浅拷贝创建一个新对象,其属性值是原始对象属性的精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是引用类型,拷贝的就是内存地址。

3.2 实现方法

  1. Object.assign()
JavaScript
const original = { a: 1, b: { c: 2 } };
const copy = Object.assign({}, original);
  1. 展开运算符
JavaScript
const original = { a: 1, b: { c: 2 } };
const copy = { ...original };
  1. Array.prototype.slice()
JavaScript
const original = [1, [2, 3]];
const copy = original.slice();
  1. 自定义浅拷贝函数
JavaScript
function shallowCopy(obj) {
  if (typeof obj !== 'object' || obj === null) return obj;
  const newObj = Array.isArray(obj) ? [] : {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      newObj[key] = obj[key];
    }
  }
  return newObj;
}

4. 深拷贝

4.1 定义

深拷贝会递归复制所有层级的属性,创建一个完全独立的新对象,新对象和原对象不共享任何内存。

4.2 实现方法

  1. JSON.parse(JSON.stringify())
JavaScript
const original = { a: 1, b: { c: 2 } };
const copy = JSON.parse(JSON.stringify(original));

注意:这种方法有局限性,不能处理函数、循环引用、Symbol等特殊情况。

  1. 递归实现
JavaScript
function deepCopy(obj, hash = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj;
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  
  if (hash.has(obj)) return hash.get(obj);
  
  const newObj = Array.isArray(obj) ? [] : {};
  hash.set(obj, newObj);
  
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      newObj[key] = deepCopy(obj[key], hash);
    }
  }
  
  return newObj;
}
  1. 使用第三方库
  • Lodash: _.cloneDeep()
  • jQuery: $.extend(true, {}, obj)

5. 浅拷贝vs深拷贝

5.1 主要区别

  1. 复制深度:

    • 浅拷贝:只复制一层
    • 深拷贝:递归复制所有层级
  2. 内存分配:

    • 浅拷贝:共享部分内存(引用类型属性)
    • 深拷贝:完全独立的内存
  3. 性能:

    • 浅拷贝:较快
    • 深拷贝:较慢,特别是对于大型复杂对象
  4. 使用场景:

    • 浅拷贝:适用于只需要复制对象第一层属性的情况
    • 深拷贝:需要完全独立的对象副本时使用

5.2 示例比较

JavaScript
const original = { a: 1, b: { c: 2 } };

// 浅拷贝
const shallowCopy = { ...original };
shallowCopy.b.c = 3;
console.log(original.b.c); // 输出: 3

// 深拷贝
const deepCopy = JSON.parse(JSON.stringify(original));
deepCopy.b.c = 4;
console.log(original.b.c); // 输出: 3

6. 面试题精选

Q1: 什么是浅拷贝和深拷贝?它们的主要区别是什么?

A1: 浅拷贝创建一个新对象,其属性值是原始对象属性的精确拷贝。对于引用类型的属性,浅拷贝只复制引用。深拷贝则会递归复制所有层级的属性,创建一个完全独立的新对象。

主要区别:

  1. 复制深度:浅拷贝只复制一层,深拷贝递归复制所有层级。
  2. 内存分配:浅拷贝的新对象与原对象共享部分内存(引用类型属性),而深拷贝创建完全独立的内存。
  3. 修改影响:修改浅拷贝对象可能影响原对象(针对引用类型属性),而修改深拷贝对象不会影响原对象。

Q2: 如何实现一个深拷贝函数?请考虑各种边界情况

A2: 这是一个可能的实现,考虑了多种边界情况:

JavaScript
function deepCopy(obj, hash = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj;
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  
  // 处理循环引用
  if (hash.has(obj)) return hash.get(obj);
  
  const newObj = Array.isArray(obj) ? [] : {};
  hash.set(obj, newObj);
  
  // 处理 Symbol 类型的key
  const symbolKeys = Object.getOwnPropertySymbols(obj);
  if (symbolKeys.length) {
    symbolKeys.forEach(symKey => {
      newObj[symKey] = deepCopy(obj[symKey], hash);
    });
  }
  
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      newObj[key] = deepCopy(obj[key], hash);
    }
  }
  
  return newObj;
}

这个实现考虑了以下边界情况:

  • 基本类型和null的处理
  • Date 和 RegExp 对象的处理
  • 循环引用的处理(使用 WeakMap)
  • Symbol 类型键的处理
  • 数组的处理

Q3: JSON.parse(JSON.stringify()) 实现深拷贝有什么缺点?

A3: 使用 JSON.parse(JSON.stringify()) 进行深拷贝虽然简单,但有以下缺点:

  1. 无法处理函数:函数会被忽略。
  2. 无法处理 undefined:undefined 值的属性会被忽略。
  3. 无法处理 Symbol:Symbol 类型的属性会被忽略。
  4. 无法处理 BigInt:会抛出 TypeError。
  5. 无法处理循环引用:会抛出错误。
  6. 无法正确处理 Date 对象:会被转换为字符串。
  7. 无法处理 RegExp、Error 对象:会被转换为空对象。
  8. 无法处理 NaN、Infinity 和 -Infinity:会被转换为 null。
  9. 只能序列化可枚举的属性。

Q4: 在React中,如何正确地复制状态对象?为什么不应该直接修改状态?

A4: 在React中,应该使用浅拷贝来复制状态对象,因为React使用浅比较来决定是否重新渲染组件。直接修改状态可能导致组件不更新。

正确的方式:

JavaScript
// 对象
this.setState(prevState => ({
  ...prevState,
  someProperty: newValue
}));

// 数组
this.setState(prevState => ({
  arr: [...prevState.arr, newItem]
}));

不应该直接修改状态的原因:

  1. 违反React的不可变性原则
  2. 可能导致难以追踪的bug
  3. 影响性能优化(如 PureComponent 和 React.memo)
  4. 使得时间旅行调试变得困难

7. 实践建议

  1. 优先使用浅拷贝:在大多数情况下,浅拷贝足以满足需求,且性能更好。
  2. 慎用 JSON 方法进行深拷贝:只有在确定对象结构简单,且不包含特殊值(如函数、undefined等)时才使用。
  3. 考虑使用第三方库:对于复杂的深拷贝需求,考虑使用如 Lodash 的 _.cloneDeep() 方法,它们通常更可靠且性能优化。
  4. 自定义深拷贝函数:如果经常需要深拷贝,可以根据项目需求自定义一个深拷贝函数,并进行充分测试。
  5. 在React应用中,遵循不可变性原则:使用展开运算符或 Object.assign() 创建新对象,而不是直接修改现有对象。
  6. 性能考虑:对于大型对象,考虑是否真的需要完整的深拷贝,可能部分浅拷贝就足够了。
  7. 测试:编写单元测试来验证你的拷贝函数是否正确处理了所有边界情况。

通过深入理解浅拷贝和深拷贝,你不仅能在面试中表现出色,还能在日常开发中写出更加健壮和高效的代码。记住,选择合适的拷贝方法取决于具体的使用场景和性能需求。