标准答案
- 基础类型可以直接返回,引用类型需要创建新的容器再递归复制。
- 循环引用必须用 WeakMap 保存原对象到副本的映射,命中缓存时直接返回副本。
- 数组和普通对象可以递归复制属性,Symbol key 也要考虑。
- Date、RegExp、Map、Set 不能简单当普通对象复制,要按各自结构创建新实例。
- 函数、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 节点、类实例和响应式对象的处理边界。