深拷贝
面试题
手写一个 deepClone,要求能处理对象、数组、循环引用、Date、RegExp、Map、Set
面试官视角
这题的核心不是背代码,而是看你是否知道深拷贝的边界:
- 基本类型直接返回
- 对象和数组需要递归拷贝
- 循环引用必须用
WeakMap记录已经拷贝过的对象 - 特殊对象不能简单当普通对象处理,比如
Date、RegExp、Map、Set - 如果追问更深,可以补充
Reflect.ownKeys能拷贝Symbol key
为什么 JSON 不行
JSON.parse(JSON.stringify(obj))这种方式只能处理普通 JSON 数据,会丢失很多信息:
| 数据 | 问题 |
|---|---|
undefined、函数、Symbol | 会丢失 |
Date | 会变成字符串 |
RegExp | 会变成 {} |
Map、Set | 会变成 {} |
| 循环引用 | 直接报错 |
| 原型 | 无法保留 |
核心思路
- 如果是基本类型或
null,直接返回 - 如果是
Date、RegExp等特殊对象,单独创建新实例 - 如果
WeakMap中已经存在当前对象,说明遇到循环引用,直接返回缓存对象 - 根据原对象类型创建空壳:数组创建
[],对象用Object.create(Object.getPrototypeOf(target))保留原型 - 把"原对象 → 克隆对象"放进
WeakMap - 遍历自有 key,递归拷贝每个属性
推荐实现
function deepClone(target, cache = new WeakMap()) {
if (target === null || typeof target !== 'object') {
return target
}
if (cache.has(target)) {
return cache.get(target)
}
if (target instanceof Date) {
return new Date(target)
}
if (target instanceof RegExp) {
const clonedReg = new RegExp(target.source, target.flags)
clonedReg.lastIndex = target.lastIndex
return clonedReg
}
if (target instanceof Map) {
const clonedMap = new Map()
cache.set(target, clonedMap)
target.forEach((value, key) => {
clonedMap.set(deepClone(key, cache), deepClone(value, cache))
})
return clonedMap
}
if (target instanceof Set) {
const clonedSet = new Set()
cache.set(target, clonedSet)
target.forEach((value) => {
clonedSet.add(deepClone(value, cache))
})
return clonedSet
}
const clonedTarget = Array.isArray(target)
? []
: Object.create(Object.getPrototypeOf(target))
cache.set(target, clonedTarget)
Reflect.ownKeys(target).forEach((key) => {
clonedTarget[key] = deepClone(target[key], cache)
})
return clonedTarget
}验证用例
const obj = {
name: 'byte',
date: new Date(),
reg: /abc/g,
arr: [1, { a: 2 }],
map: new Map(),
set: new Set(),
[Symbol('id')]: 100
}
obj.self = obj
obj.map.set({ key: 1 }, { value: 2 })
obj.set.add({ x: 1 })
const cloned = deepClone(obj)
console.log(cloned !== obj) // true
console.log(cloned.arr !== obj.arr) // true
console.log(cloned.self === cloned) // true
console.log(cloned.date instanceof Date) // true
console.log(cloned.reg instanceof RegExp) // true如果面试官继续追问
1. WeakMap 为什么比 Map 更合适?
WeakMap 的 key 只能是对象,并且是弱引用。克隆过程结束后,如果原对象没有其他引用,垃圾回收可以正常回收,避免缓存导致内存泄漏
2. 函数要不要深拷贝?
一般不拷贝函数,直接返回原函数引用。函数本身可执行逻辑和闭包环境很难真正复制,业务中也很少要求克隆函数
3. 如何保留属性描述符?
上面的实现会拷贝值,但不会完整保留 getter、setter、writable、enumerable 等属性描述符。更严谨可以用 Object.getOwnPropertyDescriptors 配合 Object.defineProperties,但面试手写通常先保证主流程和循环引用
面试回答模板
我会先判断基本类型,基本类型直接返回;对象类型再按特殊对象分支处理。为了防止循环引用,我用 WeakMap 缓存已经克隆过的对象。普通对象创建空壳后先放入缓存,再递归拷贝属性,这样即使属性里又引用到自己,也能直接从缓存里拿到克隆对象
