标准答案

  1. 基础类型可以直接返回,引用类型需要创建新的容器再递归复制。
  2. 循环引用必须用 WeakMap 保存原对象到副本的映射,命中缓存时直接返回副本。
  3. 数组和普通对象可以递归复制属性,Symbol key 也要考虑。
  4. Date、RegExp、Map、Set 不能简单当普通对象复制,要按各自结构创建新实例。
  5. 函数、DOM 节点、类实例、响应式对象等边界要提前说明,不要承诺一个万能深拷贝。

题目解析

这道题的重点不是把代码写得越长越好,而是讲清楚“哪些值需要新引用、哪些值应该保留引用、哪些类型需要特殊分支”。普通对象和数组只是最基础的部分。

循环引用是区分简单递归和可用实现的关键。对象 A 引用对象 B,B 又引用 A,如果没有缓存已访问对象,递归会一直走下去,最后栈溢出。

真实项目里也不应该无脑深拷贝大对象。深拷贝会带来性能成本,还可能破坏原型、访问器属性、响应式代理和业务对象语义。

代码示例

下面的版本覆盖常见引用类型和循环引用,仍然属于面试可讲清的简化实现。

JavaScript
function deepClone(value, cache = new WeakMap()) {
  if (value === null || typeof value !== 'object') {
    return value
  }

  if (cache.has(value)) {
    return cache.get(value)
  }

  if (value instanceof Date) {
    return new Date(value.getTime())
  }

  if (value instanceof RegExp) {
    return new RegExp(value.source, value.flags)
  }

  if (value instanceof Map) {
    const result = new Map()
    cache.set(value, result)
    value.forEach((mapValue, mapKey) => {
      result.set(deepClone(mapKey, cache), deepClone(mapValue, cache))
    })
    return result
  }

  if (value instanceof Set) {
    const result = new Set()
    cache.set(value, result)
    value.forEach(item => {
      result.add(deepClone(item, cache))
    })
    return result
  }

  const result = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value))
  cache.set(value, result)

  Reflect.ownKeys(value).forEach(key => {
    result[key] = deepClone(value[key], cache)
  })

  return result
}

常见误区

  • 把 JSON.stringify / JSON.parse 当成完整深拷贝,导致 Date、undefined、函数、Symbol、Map 等信息丢失。
  • 递归时没有 WeakMap 缓存,遇到循环引用直接栈溢出。
  • 没有说明函数、DOM 节点、类实例和响应式对象的处理边界。

作者信息