详细介绍JavaScript中的深拷贝、浅拷贝

2025-02-11 11:18:41

在 JavaScript 中,深拷贝(Deep Copy)浅拷贝(Shallow Copy) 是两种常见的对象复制方法,它们的区别主要在于如何处理对象中的引用类型数据(如对象、数组等)。

一. 浅拷贝(Shallow Copy)

浅拷贝是一种复制对象的方式,它只会复制对象的第一层属性。如果对象的某个属性是引用类型(例如对象或数组),浅拷贝只是复制了这个属性的引用对象(内存地址),而不是它的值。这意味着,拷贝对象和原始对象中的引用类型数据会共享同一个内存地址,修改拷贝对象中的引用类型数据时,会影响原始对象,反之亦然。

浅拷贝的常见方式:

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,原对象也被影响

2. 扩展运算符(...)

const obj1 = { a: 1, b: { c: 2 } }
const obj2 = { ...obj1 }

obj2.b.c = 3
console.log(obj1.b.c) // 输出 3,原对象也被影响

3. Array.prototype.slice()(对于数组)

const arr1 = [1, 2, [3, 4]]
const arr2 = arr1.slice()

arr2[2][0] = 99
console.log(arr1[2][0]) // 输出 99,原数组也被影响

4. 循环遍历

比如,可以通过 for in、for of 等循环遍历对象。

二. 深拷贝(Deep Copy)

深拷贝是复制对象的另一种方式,它会递归地拷贝对象及其所有嵌套的引用类型属性,确保新对象和原对象完全独立。换句话说,深拷贝会为每个引用类型的数据创建一个新的副本,这样修改新对象中的引用类型数据不会影响原对象。

深拷贝的常见方式:

1. JSON.parse(JSON.stringify())(适用于没有循环引用的普通对象)

const obj1 = { a: 1, b: { c: 2 } }
const obj2 = JSON.parse(JSON.stringify(obj1))

obj2.b.c = 3
console.log(obj1.b.c) // 输出 2,原对象没有变化

优点

  • 简单、快捷

  • 适用于普通的对象和数组

缺点

  • 无法拷贝 undefinedSymbolFunction 等类型的数据
const obj = {
  a: undefined,
  b: Symbol(),
  c: () => {}
}
console.log(JSON.stringify(obj))
// "{}"
  • 不能正确拷贝 DateRegExp 对象,NaN 等特殊值
const obj = {
  d: new Date(),
  e: /d+/,
  f: NaN
}
console.log(JSON.stringify(obj))
// "{"d":"2025-02-10T09:54:24.878Z","e":{},"f":null}"
  • 如果对象中有循环引用,JSON.parse() 会报错
// 循环引用
const obj = {}
obj.a = obj
console.log(JSON.stringify(obj))

错误信息:

VM55:3 Uncaught TypeError: Converting circular structure to JSON
    --> starting at object with constructor "Object"
    --- property "a" closes the circle
    at JSON.stringify (<anonymous>)
    at <anonymous>:3:18

解决方式:

通常需要维护一个“已访问”的对象列表。当你试图拷贝一个对象时,你首先检查这个对象是否已经被拷贝过了。如果是,你直接返回之前拷贝过的新对象的引用,不需要重新拷贝它。具体实现方式,可以查看以下手动递归深拷贝

2. 手写递归深拷贝

function deepClone(obj, visited = new Map()) {
  if (obj === null || typeof obj !== "object") {
    return obj // 基本类型直接返回
  }

  // 处理 Date 类型
  if (obj instanceof Date) return new Date(obj)

  // 处理 RegExp 类型
  if (obj instanceof RegExp) return new RegExp(obj)

  // 检查是否循环引用
  if (visited.has(obj)) {
    return visited.get(obj)
  }

  // 创建新的对象或数组
  const clone = Array.isArray(obj) ? [] : {}
  // 将正在拷贝的对象加入已访问列表
  visited.set(obj, clone)

  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] = deepClone(obj[key], visited) // 递归拷贝
    }
  }
  return clone
}

const obj1 = { a: 1, b: { c: 2 } }
const obj2 = deepClone(obj1)

obj2.b.c = 3
console.log(obj1.b.c) // 输出 2,原对象没有变化

优点

  • 可以处理复杂的对象,包括各种类型的属性
  • 可以扩展处理特殊类型,如 DateRegExp

缺点

  • 需要手写递归,可能存在性能问题,尤其是当对象嵌套层次很深时

  • 需要处理循环引用(需要额外的处理)

3. lodash

可以使用 lodash工具库中的cloneDeep方法。

4. structuredClone()(现代浏览器支持)

structuredClone() 是现代浏览器提供的一个原生 API,能够深拷贝大部分数据类型,支持 DateRegExpMapSetArrayBuffer 等。

const obj1 = { a: 1, b: { c: 2 } }
const obj2 = structuredClone(obj1)

obj2.b.c = 3
console.log(obj1.b.c) // 输出 2,原对象没有变化

优点

  • 支持各种数据类型(包括 DateRegExpMapSet 等)

  • 性能优秀,适用于大多数现代浏览器

缺点

  • 不支持 Function 和某些特殊对象(如 Error

  • 只在现代浏览器中支持

三. 浅拷贝 vs 深拷贝

特性 浅拷贝 深拷贝
复制内容 仅复制第一层属性,引用类型复制的是引用 递归复制所有属性,包括嵌套的引用类型数据
修改副本 如果修改了引用类型属性,原对象也会受影响 修改副本的引用类型数据,不影响原对象
性能 性能较好,因为只复制了浅层数据 性能较差,因为需要递归拷贝所有层次的数据
适用场景 对象较浅且不需要独立修改的场景 需要完全独立的数据副本的场景

四. 总结

  • 浅拷贝:适用于对象结构较简单且不需要深度复制的场景(如浅层属性直接赋值)。常用方法有 Object.assign() 和扩展运算符 {...}

  • 深拷贝:适用于对象结构较复杂,尤其是包含嵌套对象、数组等引用类型的场景。常用方法有 JSON.parse(JSON.stringify()) 和递归深拷贝。

选择深拷贝或浅拷贝,取决于数据结构的复杂度以及你是否需要确保拷贝对象和原对象完全独立。

目录

相关推荐
JavaScript之 in 运算符及其应用对象属性描述对象-JavaScript浅析 URLSearchParams 使用深入理解JS函数去抖