JavaScript 深拷贝与浅拷贝完全指南

357 阅读4分钟

JavaScript 深拷贝与浅拷贝完全指南

前言:从一段代码说起

让我们先看一段揭示拷贝本质的代码:

// 基本类型拷贝
let a = 1
let b = a
a = 2
console.log(b) // 输出:1(b 的值不受 a 后续变化影响)

// 引用类型拷贝
let obj = { age: 18 }
let obj2 = obj
obj.age = 20
console.log(obj2.age) // 输出:20(obj2 的属性随 obj 变化)

这段代码揭示了 JavaScript 中一个核心概念:基本类型是按值拷贝,而引用类型是按引用拷贝。当我们需要完整复制引用类型数据时,就涉及到浅拷贝深拷贝的概念。

正文

1. 什么是浅拷贝与深拷贝?

浅拷贝(Shallow Copy)
  • 定义:创建一个新对象,复制原始对象的顶层属性。如果属性是基本类型,则复制值;如果是引用类型,则复制内存地址(共享同一引用),简单来说就是修改原来的对象会影响到拷贝对象就是浅拷贝,相反,修改原来的对象不会影响到拷贝对象就是深拷贝
  • 特点
    • 只复制对象的第一层
    • 嵌套对象仍然是共享的
    • 修改嵌套对象会影响原对象和拷贝对象
深拷贝(Deep Copy)
  • 定义:创建一个新对象,递归复制原始对象的所有层级属性,完全切断与原始对象的引用关系
  • 特点
    • 复制对象的所有层级
    • 嵌套对象也是独立的副本
    • 修改任何层级都不会影响原对象

2. 浅拷贝的实现方法

2.1 Object.create()
const original = { a: 1, b: { c: 2 } }
const copy = Object.create(original)
// 注意:这种方式创建的对象会以 original 作为原型
2.2 Object.assign()
const original = { a: 1, b: { c: 2 } }
const copy = Object.assign({}, original)
// 修改 copy.b.c 会影响 original.b.c
2.3 数组方法:concat() 和 slice()
const arr = [1, 2, { a: 3 }]
const copy1 = arr.concat()
const copy2 = arr.slice()
// 修改 copy1[2].a 会影响原数组
2.4 数组解构
const arr = [1, 2, { a: 3 }]
const copy = [...arr]
// 修改 copy[2].a 会影响原数组
2.5 toReversed() + reverse()
const arr = [1, 2, { a: 3 }]
const copy = arr.toReversed().reverse()
// ES2023 新方法,同样是浅拷贝

3. 深拷贝的实现方法

3.1 JSON 序列化法(最常用)
const original = { a: 1, b: { c: 2 } }
const copy = JSON.parse(JSON.stringify(original))
// 完全独立的副本

缺点

  1. 无法处理特殊类型:
    • undefined → 丢失
    • function → 丢失
    • Symbol → 丢失
    • BigInt → 报错
  2. 无法处理循环引用:
    const obj = { a: 1 }
    obj.self = obj
    JSON.parse(JSON.stringify(obj)) // 报错
    
  3. 会丢失原型链信息
3.2 手动实现深拷贝
function deepClone(obj, hash = new WeakMap()) {
  // 处理基本类型和 null/undefined
  if (obj === null || typeof obj !== 'object') {
    return obj
  }
  
  // 处理循环引用
  if (hash.has(obj)) {
    return hash.get(obj)
  }
  
  // 处理 Date 和 RegExp
  if (obj instanceof Date) return new Date(obj)
  if (obj instanceof RegExp) return new RegExp(obj)
  
  // 创建新对象/数组
  const cloneObj = new obj.constructor()
  hash.set(obj, cloneObj)
  
  // 递归拷贝属性
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      cloneObj[key] = deepClone(obj[key], hash)
    }
  }
  
  return cloneObj
}

4. 如何选择拷贝方式?

场景推荐方法
只需要第一层拷贝浅拷贝方法
需要完全独立副本深拷贝方法
简单对象,无特殊类型JSON.parse(JSON.stringify())
复杂对象,有函数/Symbol等手动实现或 Lodash
考虑性能的大对象结构化克隆算法(如 MessageChannel

5. 性能考量

  • 浅拷贝:性能高,时间复杂度 O(n)
  • 深拷贝:性能较低,时间复杂度 O(n) + 递归开销
  • JSON 方法:对于大对象性能较差(需要序列化和解析)
  • 手动实现:可以通过 WeakMap 优化循环引用检测

结语

理解深浅拷贝是 JavaScript 开发者的必备技能。在实际开发中:

  1. 优先使用浅拷贝:当确定只需要第一层拷贝时
  2. 谨慎选择深拷贝方法:根据数据结构特点选择合适方案
  3. 处理循环引用:使用 WeakMap 或现成库函数
  4. 注意特殊类型:函数、Symbol 等需要特殊处理

记住这个简单的原则:当你需要完全独立的副本时,必须使用深拷贝。对于大多数日常需求,JSON.parse(JSON.stringify()) 已经足够,但在复杂场景下,考虑使用 Lodash 等库函数或自己实现健壮的深拷贝函数。

掌握这些知识后,你将能够:

  • 避免意外的引用共享导致的 bug
  • 在需要时创建完全独立的数据副本
  • 在面试中游刃有余地回答相关问题

祝你在 JavaScript 的深拷贝与浅拷贝世界中探索愉快!🚀