手写深拷贝与浅比较深度解析:为什么 structuredClone 也不是万能药?
深拷贝难点在循环引用(WeakMap 记忆化)、20 多种内置类型兼容、迭代栈防爆栈;浅比较核心是 Object.is 语义。 面试考类型边界:NaN 与 Object.is(NaN,NaN) 为 true 这类细节。
一句话概括
深拷贝的难度不在于「递归复制属性」,而在于处理循环引用(WeakMap 做记忆化)、兼容 Map/Set/Date/RegExp/ArrayBuffer 等 20+ 种内置类型、以及避免栈溢出(迭代栈替代递归);浅比较的核心则是 Object.is 的精准语义——NaN === NaN 是 false 但 Object.is(NaN, NaN) 是 true。
核心知识点
1. 浅拷贝 vs 深拷贝的本质
1
2
3
4
5
6
7
8
9
10
11
const obj = { a: 1, b: { c: 2 } }
// 引用赋值:改一个影响另一个
const ref = obj; ref.b.c = 99 // obj.b.c 也变成 99
// 浅拷贝:只复制第一层
const shallow = { ...obj }; shallow.b.c = 88 // obj.b.c 也变成 88!
shallow.a = 10 // obj.a 仍是 1 ✅
// 深拷贝:所有层级独立
const deep = structuredClone(obj); deep.b.c = 77 // obj.b.c 还是 2 ✅
2. JSON.parse(JSON.stringify()) 的 7 宗罪
面试高频考点——用这个方案会被追问「那你怎么解决」:
1
2
3
4
5
6
7
8
9
10
const data = {
date: new Date(), // → 变成字符串 "2026-07-17T..."
regex: /hello/gi, // → 变成 {}
fn: () => {}, // → 被丢弃
undef: undefined, // → 被丢弃
sym: Symbol('x'), // → 被丢弃
map: new Map([['k',1]]),// → 变成 {}
nan: NaN, // → 变成 null!
big: 9007199254740993n, // → TypeError: BigInt 无法序列化
}
3. 生产级深拷贝:覆盖所有内置类型 + 处理循环引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
function deepClone(obj, map = new WeakMap()) {
// 基本类型 / null / function(函数通常不拷贝)
if (obj === null || typeof obj !== 'object') return obj
if (typeof obj === 'function') return obj
// 循环引用检测(核心!)
if (map.has(obj)) return map.get(obj)
// Date / RegExp
if (obj instanceof Date) return new Date(obj)
if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags)
// Map
if (obj instanceof Map) {
const m = new Map(); map.set(obj, m)
obj.forEach((v, k) => m.set(deepClone(k, map), deepClone(v, map)))
return m
}
// Set
if (obj instanceof Set) {
const s = new Set(); map.set(obj, s)
obj.forEach(v => s.add(deepClone(v, map)))
return s
}
// TypedArray / ArrayBuffer
if (ArrayBuffer.isView(obj)) {
return new obj.constructor(
obj.buffer.slice(0), obj.byteOffset, obj.length
)
}
// Array / Object — 使用 for...in + hasOwnProperty(最兼容)
const result = Array.isArray(obj) ? [] : {}
map.set(obj, result)
for (const key of Object.keys(obj)) {
result[key] = deepClone(obj[key], map)
}
// 别漏了 Symbol 属性
for (const key of Object.getOwnPropertySymbols(obj)) {
result[key] = deepClone(obj[key], map)
}
return result
}
核心技巧:WeakMap 做记忆化——map.set(obj, result) 记录「原始对象→克隆对象」的映射,后续再遇到同一引用直接返回克隆版本。WeakMap 不会阻止 GC,所以不会泄漏内存。
4. 避免栈溢出:迭代替代递归
递归深拷贝遇到深层嵌套(>10000 层)会爆栈:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function deepCloneIterative(obj) {
const root = Array.isArray(obj) ? [] : {}
const stack = [{ source: obj, target: root }]
const map = new WeakMap([[obj, root]])
while (stack.length) {
const { source, target } = stack.pop()
for (const key of Object.keys(source)) {
const val = source[key]
if (val !== null && typeof val === 'object') {
if (map.has(val)) { target[key] = map.get(val); continue }
if (val instanceof Date) { target[key] = new Date(val); continue }
if (val instanceof RegExp) { target[key] = new RegExp(val.source, val.flags); continue }
const clone = Array.isArray(val) ? [] : {}
map.set(val, clone); target[key] = clone
stack.push({ source: val, target: clone })
} else {
target[key] = val
}
}
}
return root
}
5. 浅比较:React.memo 的幕后功臣
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function shallowEqual(a, b) {
// 用 Object.is 而非 ===,因为 Object.is(NaN, NaN) 是 true
if (Object.is(a, b)) return true
if (typeof a !== 'object' || a === null ||
typeof b !== 'object' || b === null) return false
const keysA = Object.keys(a), keysB = Object.keys(b)
if (keysA.length !== keysB.length) return false
for (const key of keysA) {
if (!Object.prototype.hasOwnProperty.call(b, key)) return false
if (!Object.is(a[key], b[key])) return false
}
return true
}
// React.memo 的默认比较就是这种浅比较
// 这就是为什么父组件传新对象字面量就会导致子组件重渲染
const Parent = () => {
const data = { name: 'Alice' } // 每次 render 新引用 → memo 失效
return <Child data={data} />
}
解决方案:useMemo 稳定引用:
1
const data = useMemo(() => ({ name: 'Alice' }), [])
其实你每天都在用
- React 状态更新 —
setState(newValue)要求newValue是全新对象,React 靠浅比较判断prev !== next来决定重渲染——你拷贝错了一个嵌套层级,组件就不更新了 - Redux reducer —
return { ...state, user: { ...state.user, name: 'Bob' } }这套「展开运算符体操」就是在做不可变更新(浅拷贝),少展开一层就会出现 bug - Vue watch 的深度监听 —
watch(source, callback, { deep: true })内部就是在做深比较,对象里任何叶子变了都触发 - 配置合并 —
Object.assign({}, defaults, userConfig)是浅合并,所以用户只传了一层配置就能覆盖所有默认值,但嵌套配置需要merge - 剪贴板/DevTools — Chrome DevTools 的「Copy object」、
console.log(obj)之后在控制台改属性不影响原对象——DevTools 内部用的也是序列化+反序列化方式传递数据,本质上是一次深拷贝
常见误解(FAQ)
❌ 误区:「structuredClone 能解决一切深拷贝问题」
structuredClone 确实比 JSON 方案强得多(支持 Map/Set/Date/RegExp/ArrayBuffer/循环引用),但仍有局限:不支持 Function/Symbol/WeakMap/DOM 节点,遇到会抛 DataCloneError。而且对象原型会被重置为 Object.prototype(class 实例的方法丢失)。
❌ 误区:「浅比较只要比较对象的所有 key 值相等就行」
还要比较两个对象是否是同一个引用(objA === objB)——如果引用相同,直接返回 true,省去遍历。另一个隐秘陷阱:key 的数量必须相等——{ a: 1 } 和 { a: 1, b: 2 } 不能说只比 a 相等就算相等。
❌ 误区:「Object.assign 和展开运算符 {…obj} 是深拷贝」
二者都只做一层复制。{ ...obj } 只会复制 obj 自身的可枚举属性到新对象,嵌套对象仍然是原来的引用。JSON.parse(JSON.stringify(obj)) 也不是真正的深拷贝。
❌ 误区:「手写深拷贝时用 Map 记录已访问对象就足够处理循环引用了」
应该用 WeakMap 而非 Map。Map 的 key 会阻止 GC,如果你深拷贝一个图结构(大量对象相互引用),Map 会阻止所有这些对象被回收。WeakMap 的 key 是弱引用,不影响 GC。
一句话总结
深拷贝和浅比较,一个是「完全复制一个对象图」的工程难题,一个是「快速判断两个对象是否表层一致」的性能优化——前者不丢数据是关键,后者不引起假更新是关键。理解了这两者,React 的性能优化、Redux 的不可变原则、以及 JS 引用类型的本质才算真正入门。