深拷贝

为什么不能直接用 JSON.stringify

JSON.parse(JSON.stringify(obj)) 只适合简单的纯数据对象。缺陷:

  • 遇到循环引用直接抛 Converting circular structure to JSON
  • 丢失函数、undefined、Symbol
  • Date 变成字符串
  • Map/Set 变成 {}
  • RegExp 变成空对象
  • 无法保留原型链

手写实现

核心思路:递归 + WeakMap 记录已拷贝的对象,遇到已拷贝的直接返回引用,避免循环引用。

function deepClone(origin, visited = new WeakMap()) {
  // 基本类型直接返回
  if (origin == null || typeof origin !== 'object') {
    return origin
  }
 
  // Date 类型
  if (origin instanceof Date) {
    return new Date(origin)
  }
 
  // 正则类型
  if (origin instanceof RegExp) {
    return new RegExp(origin)
  }
 
  // Map 类型
  if (origin instanceof Map) {
    const clone = new Map()
    visited.set(origin, clone)
    origin.forEach((val, key) => {
      clone.set(deepClone(key, visited), deepClone(val, visited))
    })
    return clone
  }
 
  // Set 类型
  if (origin instanceof Set) {
    const clone = new Set()
    visited.set(origin, clone)
    origin.forEach((val) => {
      clone.add(deepClone(val, visited))
    })
    return clone
  }
 
  // 检查循环引用:已拷贝过直接返回
  if (visited.has(origin)) {
    return visited.get(origin)
  }
 
  // 数组或对象,用原对象的构造函数构造目标
  const target = new origin.constructor()
  visited.set(origin, target)
 
  for (let key in origin) {
    // 只拷贝自身属性,排除原型链上的属性
    if (origin.hasOwnProperty(key)) {
      target[key] = deepClone(origin[key], visited)
    }
  }
 
  return target
}

关键点

  • WeakMap 而不是 Map:WeakMap 的 key 是对象引用,不阻止 GC,且 key 可以是对象。Map 的 key 如果是对象,会阻止该对象被 GC。
  • 先检查循环引用再递归:进入对象前先查 visited,有就直接返回,避免无限递归。
  • 原始值直接返回:string/number/boolean/null/undefined 不需要拷贝。
  • new origin.constructor():用原对象的构造函数构造目标,数组和对象统一处理。
  • hasOwnProperty 过滤原型链属性:for...in 会遍历原型链上的属性,用 hasOwnProperty 只拷贝自身属性。

测试

// 循环引用
const a = { name: 'a' }
a.self = a
const b = deepClone(a)
console.log(b.name)        // 'a'
console.log(b.self === b)  // true(循环引用正确处理)
 
// Date
const c = { date: new Date() }
const d = deepClone(c)
console.log(d.date instanceof Date)  // true(不是字符串)
 
// 原始值
console.log(deepClone(42))       // 42
console.log(deepClone('hello'))  // 'hello'
console.log(deepClone(null))     // null

常见问题

  • 没有处理循环引用,遇到 a.self = a 会无限递归栈溢出