⭐ 推荐:社区规则条款 V1.0

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

dp32323
dp32323 正式会员正式会员
发布于 2026-09-23 01:39 ·97 浏览 ·0 回复
内容摘要

按从简到全的顺序梳理 JavaScript 深拷贝的 8 种写法,给出最小可用代码,说明各自适用场景,以及在循环引用、Date、Map、Set、函数、Symbol 等特殊类型上的数据丢失情况。

学完这篇,你能拿到一份可直接抄的 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
转载请注明出处,版权归原作者所有。
他们都看过 1 人浏览过
CLARA轻量论坛系统

全部回复 0

还没有回复,来抢沙发~