JavaScript 深拷贝的 8 种写法:structuredClone 之外还有谁

dp32323
dp32323 正式会员
发布于 2026-09-23 01:39 ·3 浏览 ·0 回复

学完这篇,你能拿到一份可直接抄的 JavaScript 深拷贝清单:8 种写法各自能干什么、会丢什么数据、什么时候该用哪一种,看完不用再翻十几篇博客。

深拷贝的核心难点从来不是"递归写不出来",而是循环引用、特殊类型、函数与原型这三件事的处理。下面按"从简到全"的顺序排,每条都给最小可用代码和踩坑点。

方法一:JSON.parse(JSON.stringify(obj))

最省事的一行流:

const copy = JSON.parse(JSON.stringify(obj));

只适合纯数据(字符串/数字/布尔/数组/普通对象/纯嵌套)。它的丢失清单很长:`undefined`、函数、Symbol 键直接消失;`Date` 变字符串;`NaN`、`Infinity` 变 `null`;`Map`、`Set` 变 `{}`;遇到循环引用直接抛 `TypeError`。

注意:接口返回的数据里只要有 `Date` 字段,用这招日期就成字符串了,后续 `.getFullYear()` 会报错。

方法二:手写递归(基础版)

function clone(obj) {
  if (obj === null || typeof obj !== 'object') return obj;
  const target = Array.isArray(obj) ? [] : {};
  for (const key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      target[key] = clone(obj[key]);
    }
  }
  return target;
}

`for...in` 会遍历原型链上的可枚举属性,所以必须加 `hasOwnProperty` 判断。这版会丢 Symbol 键和不可枚举属性,也不认 `Date`、`Map`。

方法三:递归 + WeakMap 解决循环引用

function clone(obj, map = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj;
  if (map.has(obj)) return map.get(obj);
  const target = Array.isArray(obj) ? [] : {};
  map.set(obj, target);
  for (const key of Reflect.ownKeys(obj)) {
    target[key] = clone(obj[key], map);
  }
  return target;
}

两个升级点:`WeakMap` 缓存已克隆对象,循环引用不再死循环;`Reflect.ownKeys` 能拿到 Symbol 键和不可枚举属性。仍然不处理 `Date`、`Map` 这些内置类型。

注意:WeakMap 必须是参数传递,写成函数外的全局变量会跨调用污染。

方法四:递归 + 类型完整版

在方法三基础上补类型分支:

if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags);
if (obj instanceof Map) { /* 逐项 clone */ }
if (obj instanceof Set) { /* 逐项 clone */ }

`Map`/`Set` 要新建实例再遍历塞进去,别指望遍历索引。这版能覆盖 95% 的业务场景,代价是代码量上去了,且类实例的原型链依然会丢——克隆出来是普通对象,`instanceof` 判断失效。

方法五:structuredClone(现代首选)

const copy = structuredClone(obj);

浏览器与 Node 17+ 原生支持,底层是"结构化克隆算法",自动处理循环引用、`Date`、`RegExp`、`Map`、`Set`、`ArrayBuffer`、`Blob`、`File`,代码零成本。

它不支持的三样:函数(抛 DataCloneError)、DOM 节点、Symbol 作为键(该属性会被静默忽略,不报错)。另外它不保留原型链和 getter/setter,getter 会被求值成静态值。

方法六:MessageChannel 消息通道

结构化克隆算法的另一条通道,兼容比 `structuredClone` 更老的浏览器:

function clone(obj) {
  return new Promise((resolve) => {
    const { port1, port2 } = new MessageChannel();
    port1.onmessage = (e) => resolve(e.data);
    port2.postMessage(obj);
  });
}

能力边界和方法五一模一样,但它是异步的,调用处必须 `await`。适合本来就在异步流程里的场景。

方法七:history.pushState 偷懒法

const clone = (obj) => {
  history.pushState({}, '', location.href);
  return history.state; // 不对,正确写法见下
};

正确姿势是把数据塞进 state 再用 `history.replaceState` 比对取值,同步得到克隆结果,同样走结构化克隆。但它在 Worker 里、SSR 环境里不存在,还会污染浏览器历史记录,只适合调试时应急。

注意:这个写法在生产环境基本别用,`pushState` 会被用户按返回键时踩到。

方法八:lodash.cloneDeep

import cloneDeep from 'lodash/cloneDeep';
const copy = cloneDeep(obj);

功能最全的现成方案:循环引用、`Date`、`RegExp`、`Map`、`Set`、`ArrayBuffer`、Symbol 键、原型链都保留,函数按引用复制(不报错)。代价是引入依赖、体积增加。项目里已经有 lodash 就直接用,没有就为这一个函数单独引,性价比不高。

怎么选

场景推荐
纯 JSON 数据、临时用JSON 一行流
现代浏览器 / Node 17+structuredClone
需要保留类实例、函数、原型lodash.cloneDeep
需要精细控制类型方法四手写
老浏览器 + 异步流程MessageChannel

小结

  • 优先 `structuredClone`,它覆盖了绝大多数内置类型和循环引用,唯一的硬伤是不能克隆函数
  • JSON 方案只处理纯数据,`Date`、`undefined`、`NaN`、循环引用四类问题必踩。
  • 手写递归的关键两点:`WeakMap` 防循环、`Reflect.ownKeys` 保 Symbol 键。
  • 所有基于结构化克隆算法的方案(structuredClone / MessageChannel / history.state)共享同一套限制:函数、DOM、Symbol 键、原型链。
  • 需要原型和函数时,要么用 lodash,要么手写并显式处理原型:`Object.create(Object.getPrototypeOf(obj))`。
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-564.html
转载请注明出处,版权归原作者所有。

全部回复 0

还没有回复,来抢沙发~