深拷贝
为什么不能直接用 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会无限递归栈溢出