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)

✅ 原生支持,无需安装
✅ 同步
✅ 支持 DateMapSetArrayBuffer、循环引用
❌ 不支持函数(任何方案都不支持,见下文)
❌ 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


三句话记住最佳实践

  1. 浅拷贝用展开运算符,简单直接
  2. 深克隆用 structuredClone,现代项目标准答案
  3. 有 lodash 就用 _.cloneDeep,处理了所有边界情况

七、总结对比

方案 同步 循环引用 Date/Map/Set 函数 推荐度
{...obj} - - - ✅ 浅拷贝首选
JSON 方案 ⚠️ 纯JSON数据可用
structuredClone ✅ 深克隆首选
MessageChannel ❌ 已过时
手写递归 ⚠️ 需自己维护
lodash.cloneDeep ✅ 有lodash时用
posted @ 2022-12-29 23:54  HuangBingQuan  阅读(117)  评论(0)    收藏  举报