JavaScript 拷贝完全指南
浅拷贝、深克隆、各种方案的能力边界一次说清楚。
先理解:浅拷贝 vs 深克隆
const obj = { a: 1, b: { c: 2 } }
- 浅拷贝:复制第一层,嵌套对象还是同一个引用
- 深克隆:递归复制所有层级,完全独立
// 浅拷贝
const shallow = { ...obj }
shallow.b.c = 99
console.log(obj.b.c) // 99 ← 原对象被影响了!
// 深克隆
const deep = structuredClone(obj)
deep.b.c = 99
console.log(obj.b.c) // 2 ← 原对象不受影响 ✅
一、浅拷贝方法
1. 展开运算符 {...obj} / [...arr]
const obj2 = { ...obj }
const arr2 = [...arr]
✅ 最简洁,日常最常用
❌ 只复制第一层
2. Object.assign()
const obj2 = Object.assign({}, obj)
✅ 可以同时合并多个源对象
❌ 只复制第一层,和展开运算符等价
3. 数组专用方法
const arr2 = arr.slice()
const arr2 = Array.from(arr)
const arr2 = arr.concat()
✅ 语义清晰
❌ 只适用于数组,且只复制第一层
浅拷贝汇总
| 方法 | 适用 | 特点 |
|---|---|---|
{...obj} |
对象 | 最简洁 |
[...arr] |
数组 | 最简洁 |
Object.assign({}, obj) |
对象 | 可合并多个源 |
arr.slice() |
数组 | 语义:切片 |
Array.from(arr) |
数组 | 可带映射函数 |
二、深克隆方法
方案 1:JSON 序列化(最常见,但有缺陷)
const cloned = JSON.parse(JSON.stringify(obj))
✅ 简单,零依赖
❌ 有很多坑:
| 数据类型 | 结果 |
|---|---|
Date |
变成字符串 "2024-01-01T00:00:00.000Z" |
undefined |
直接丢失 |
Map / Set |
变成 {} 空对象 |
| 循环引用 | 直接报错 |
| 函数 | 直接丢失 |
NaN / Infinity |
变成 null |
适合:纯 JSON 数据(只有字符串、数字、布尔、普通对象、数组)
方案 2:structuredClone(推荐,现代标准)
const cloned = structuredClone(obj)
✅ 原生支持,无需安装
✅ 同步
✅ 支持 Date、Map、Set、ArrayBuffer、循环引用
❌ 不支持函数(任何方案都不支持,见下文)
❌ Chrome 98+、Node.js 17+(旧环境需 polyfill)
现在首选这个,没有理由不用。
方案 3:MessageChannel(历史方案)
const deepClone = (obj) => new Promise(resolve => {
const { port1, port2 } = new MessageChannel()
port1.postMessage(obj)
port2.onmessage = (msg) => resolve(msg.data)
})
const cloned = await deepClone(obj)
本质:借用浏览器跨线程通信内置的结构化克隆算法,和 structuredClone 底层完全一样。
✅ 解决了 JSON 方案的大部分缺陷
❌ 异步,调用方必须 await,增加复杂度
❌ 已被 structuredClone 完全替代,现在没必要用
历史背景:
structuredClone普及之前的社区 hack,理解原理即可。
方案 4:手写递归
function deepClone(obj, map = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj
if (obj instanceof Date) return new Date(obj)
if (obj instanceof RegExp) return new RegExp(obj)
if (map.has(obj)) return map.get(obj) // 处理循环引用
const cloned = Array.isArray(obj) ? [] : {}
map.set(obj, cloned)
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = deepClone(obj[key], map)
}
}
return cloned
}
✅ 可以自定义支持任意类型
✅ 可以支持函数(直接引用或复制)
❌ 需要自己维护,容易遗漏边界情况(Symbol、getter/setter 等)
方案 5:lodash _.cloneDeep()
import _ from 'lodash'
const cloned = _.cloneDeep(obj)
✅ 成熟稳定,处理了大量边界情况
✅ 支持函数(直接引用)
❌ 需要引入 lodash(但很多项目本来就有)
三、关于函数:为什么没有方案能克隆函数?
const obj = { fn: () => console.log(x) }
函数不只是代码,它还包含闭包——对外部变量的引用。克隆函数意味着要克隆它所在的整个作用域,这在技术上不可能做到完整复制。
所以:函数本来就不应该被克隆,遇到函数属性通常的处理是直接引用(浅拷贝)。
四、方案选型指南
需要深克隆?
├── 数据是纯 JSON(无 Date/Map/循环引用)
│ └── JSON.parse(JSON.stringify()) ✅ 简单够用
│
├── 数据复杂,环境支持 Chrome 98+ / Node 17+
│ └── structuredClone() ✅ 首选
│
├── 需要支持函数 / 高度定制
│ └── 手写递归 或 lodash.cloneDeep()
│
└── 看到别人用 MessageChannel 实现
└── 了解原理即可,用 structuredClone 替代
五、演进时间线
早期
└── JSON.parse(JSON.stringify())
简单但残缺
中期
└── 手写递归 / lodash.cloneDeep()
解决 JSON 方案缺陷,但需要自己实现或引库
structuredClone 普及前
└── MessageChannel 方案
借用浏览器结构化克隆能力,但异步
现在
└── structuredClone(obj)
原生、同步、能力完整,直接用
六、最佳实践
浅拷贝:永远用展开运算符
// ✅ 对象
const obj2 = { ...obj }
// ✅ 数组
const arr2 = [...arr]
为什么:语法最简洁,意图最清晰,性能最好,没有任何理由选其他方式。
深克隆:分三种情况
情况一:数据是纯 JSON(接口返回的普通数据)
const cloned = JSON.parse(JSON.stringify(obj))
为什么:零依赖,够用,接口数据几乎不会有 Date/Map/循环引用。
情况二:数据复杂(有 Date、Map、Set、循环引用等)
const cloned = structuredClone(obj)
为什么:原生支持、同步、能力完整,是目前深克隆的标准答案。不需要引库,不需要写代码,一行搞定。
情况三:项目已有 lodash,或数据里有函数
const cloned = _.cloneDeep(obj)
为什么:lodash 处理了极端边界情况(Symbol、getter/setter 等),且能保留函数引用。如果项目里本来就用 lodash,直接用,不需要再引 structuredClone。
三句话记住最佳实践
- 浅拷贝用展开运算符,简单直接
- 深克隆用
structuredClone,现代项目标准答案- 有 lodash 就用
_.cloneDeep,处理了所有边界情况
七、总结对比
| 方案 | 同步 | 循环引用 | Date/Map/Set | 函数 | 推荐度 |
|---|---|---|---|---|---|
{...obj} |
✅ | - | - | - | ✅ 浅拷贝首选 |
JSON 方案 |
✅ | ❌ | ❌ | ❌ | ⚠️ 纯JSON数据可用 |
structuredClone |
✅ | ✅ | ✅ | ❌ | ✅ 深克隆首选 |
MessageChannel |
❌ | ✅ | ✅ | ❌ | ❌ 已过时 |
| 手写递归 | ✅ | ✅ | ✅ | ✅ | ⚠️ 需自己维护 |
lodash.cloneDeep |
✅ | ✅ | ✅ | ✅ | ✅ 有lodash时用 |

浙公网安备 33010602011771号