程序员面试必问:JSON 深拷贝的 5 种实现方式

深拷贝是前端面试的高频题,也是工作中经常遇到的场景。这篇文章用 5 种方法实现 JSON 深拷贝,从最简单的到最完善的,帮你一次搞清楚。


什么是深拷贝?

在 JavaScript 中,对象是引用类型:

1
2
3
4
const obj = { name: 'javahiker', skills: ['Java', 'React'] }
const copy = obj
copy.name = 'new name'
console.log(obj.name) // 'new name' — 原对象也被改了

上面是浅拷贝,只复制了引用。深拷贝要创建一个全新的对象,与原对象完全独立。


方法 1:JSON.parse(JSON.stringify()) — 最简单的方案

1
2
3
function deepCopy(obj) {
return JSON.parse(JSON.stringify(obj))
}

优点:

  • 一行代码,极其简单
  • 大部分场景够用

缺点: ⚠️ 很多坑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const test = {
name: 'javahiker',
date: new Date(),
regex: /test/gi,
func: () => console.log('hi'),
undef: undefined,
inf: Infinity,
nan: NaN
}

const copied = JSON.parse(JSON.stringify(test))
// date → 字符串(不是 Date 对象了)
// regex → {}(空对象)
// func → 丢失
// undef → 丢失
// inf → null
// nan → null

结论: 面试中这个方法能说,但要立刻说出它的局限性。生产环境只适用于纯数据对象(没有函数、Date、RegExp、undefined 等)。

可以用在线 JSON 工具验证你的深拷贝结果:JSON 格式化工具


方法 2:递归实现 — 面试手写标准答案

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
function deepCopy(obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj

// 处理循环引用
if (hash.has(obj)) return hash.get(obj)

// 处理 Date
if (obj instanceof Date) return new Date(obj)

// 处理 RegExp
if (obj instanceof RegExp) return new RegExp(obj)

// 处理 Map
if (obj instanceof Map) {
const copy = new Map()
hash.set(obj, copy)
obj.forEach((value, key) => copy.set(deepCopy(key, hash), deepCopy(value, hash)))
return copy
}

// 处理 Set
if (obj instanceof Set) {
const copy = new Set()
hash.set(obj, copy)
obj.forEach(value => copy.add(deepCopy(value, hash)))
return copy
}

// 处理数组和普通对象
const copy = Array.isArray(obj) ? [] : {}
hash.set(obj, copy)

for (const key of Object.keys(obj)) {
copy[key] = deepCopy(obj[key], hash)
}

// 处理 Symbol 属性
for (const sym of Object.getOwnPropertySymbols(obj)) {
copy[sym] = deepCopy(obj[sym], hash)
}

return copy
}

关键点:

  • WeakMap 解决循环引用(防止 obj.self = obj 导致栈溢出)
  • 分别处理 Date、RegExp、Map、Set 等特殊类型
  • Object.keys + Object.getOwnPropertySymbols 遍历所有属性

面试加分项: 主动提到用 WeakMap 而不只是 Map,因为 WeakMap 不会阻止垃圾回收。


方法 3:structuredClone() — 现代浏览器的原生方案

1
2
// 一行代码,浏览器原生支持
const copied = structuredClone(original)

支持的:

  • ✅ 普通对象和数组
  • ✅ 嵌套对象
  • ✅ Date、RegExp、Map、Set
  • ✅ ArrayBuffer、TypedArray
  • ✅ Blob、File、ImageData
  • ✅ 循环引用(自动处理)

不支持的:

  • ❌ 函数
  • ❌ Symbol
  • ❌ WeakMap、WeakSet
  • ❌ DOM 节点
  • ❌ Error 对象

兼容性: Chrome 98+、Firefox 94+、Safari 15.4+(2022 年后的浏览器都支持)

1
2
3
4
5
6
7
8
9
10
11
12
// 实际使用
const user = {
name: 'javahiker',
createdAt: new Date(),
tags: new Set(['json', 'react', 'tools']),
preferences: new Map([['theme', 'dark']])
}

const copy = structuredClone(user)
console.log(copy.createdAt instanceof Date) // true
console.log(copy.tags instanceof Set) // true
console.log(copy.preferences instanceof Map) // true

建议: 现代项目中,没有函数属性的对象,直接用 structuredClone() 最省心。


方法 4:MessageChannel — 利用 postMessage 的序列化

1
2
3
4
5
6
7
8
9
10
function deepCopy(obj) {
return new Promise((resolve) => {
const channel = new MessageChannel()
channel.port1.onmessage = (event) => resolve(event.data)
channel.port2.postMessage(obj)
})
}

// 使用(注意是异步的)
const copy = await deepCopy(original)

原理: postMessage 在跨窗口通信时会对数据进行结构化克隆(和 structuredClone 使用相同的算法)。

优点:

  • 浏览器原生,不需要 polyfill
  • JSON.parse/stringify 支持更多类型

缺点:

  • 异步的,使用起来不如 structuredClone 方便
  • 兼容性跟 structuredClone 类似

面试价值: 提到这个说明你了解浏览器的底层机制,但实际工作用 structuredClone 就够了。


方法 5:Lodash cloneDeep — 生产环境最稳妥

1
2
3
import _ from 'lodash'

const copy = _.cloneDeep(original)

特点:

  • 处理了 99% 的边界情况
  • 经过大量项目验证,坑最少
  • 函数、Symbol 等 structuredClone 不支持的类型也能处理

唯一缺点: 增加了打包体积。但如果你已经在用 lodash,这是最稳妥的选择。

1
2
3
4
5
6
7
8
9
10
11
12
// lodash cloneDeep 支持的边界情况
const complex = {
buf: new ArrayBuffer(8),
typed: new Uint8Array([1, 2, 3]),
error: new Error('test'),
self: null // 后面会循环引用
}
complex.self = complex // 循环引用

const copy = _.cloneDeep(complex)
console.log(copy.error instanceof Error) // true
console.log(copy.self === copy) // true(循环引用正确处理了)

5 种方法对比总结

方法 代码量 浏览器兼容 函数 Date/RegExp 循环引用 推荐场景
JSON.parse/stringify ⭐⭐⭐ ⭐⭐⭐⭐⭐ 纯数据对象
递归实现 ⭐⭐⭐⭐⭐ 面试手写
structuredClone ⭐⭐⭐ ⭐⭐⭐ 现代项目首选
MessageChannel ⭐⭐ ⭐⭐⭐ 了解即可
Lodash cloneDeep ⭐⭐⭐ ⭐⭐⭐⭐⭐ 生产环境最稳

面试中怎么回答

面试官问”如何实现深拷贝”,建议的答题顺序:

  1. 先说最简单方案: JSON.parse(JSON.stringify()),立刻说出它的局限性
  2. 再说原生方案: structuredClone(),说明浏览器原生支持,2022 年后可用
  3. 手写递归版本: 重点展示 WeakMap 处理循环引用,以及分类型处理
  4. 最后提 lodash: 生产环境推荐用现成的

如果面试官追问”能不能拷贝函数”——
回答:理论上可以,用 new Function('return ' + fn.toString())(),但作用域链会丢失,实践中很少这样用。函数不应该被拷贝,应该复用。


需要在线验证你的 JSON 数据结构?试试这个免费工具箱(纯前端,不上传服务器):
JSON 工具箱 — 格式化 / 校验 / 对比 / 代码生成