lodash.cloneDeep 与原生 JS 深拷贝方案对比
深入对比 lodash.cloneDeep、原生 structuredClone、JSON 序列化及手写递归等主流深拷贝方案,从数据类型支持、循环引用、打包体积、执行性能及工程选型进行全面剖析。
lodash.cloneDeep,或借由 JSON.parse(JSON.stringify()) 完成简单场景的序列化复制。随着 Web 标准在全平台内置了 structuredClone() 原生 API,深拷贝的选型格局发生了深刻改变。本文针对这两种核心方案及手写递归方案,在支持类型、循环引用、打包体积、执行性能与工程选型上展开系统性横向对比,帮助读者在现代工程中做出严谨的技术决策。
一、深拷贝的本质难题与内存模型
在 JavaScript 中,基本数据类型(如数字、字符串、布尔值)按值存储在栈内存中,而引用数据类型(Object、Array、Map 等)的实体分配在堆内存中,变量中保存的仅是堆内存地址的引用指针。
当执行浅拷贝(如 Object.assign() 或对象展开语法 { ...obj })时,只会复制第一层属性。如果属性值为复合对象,复制的依然是指向同一块堆内存的指针。一旦修改嵌套对象中的属性,源对象的深层数据将不可避免地被同步修改,导致数据污染或不可预期的副作用。
深拷贝(Deep Clone)则要求开辟全新的独立堆内存空间,递归复制对象树的所有层级节点。要实现一个完备健壮的深拷贝,必须克服以下核心难点:
- 循环引用(Circular Reference):当对象内部属性直接或间接引用自身时,若无访问记录机制,递归算法会陷入死循环并触发
RangeError: Maximum call stack size exceeded。 - 特殊原生对象实例:诸如
Date、RegExp、Map、Set、ArrayBuffer、Error等对象不能单纯作为空普通对象{}遍历赋值,必须使用专有构造函数或原生序列化逻辑重建。 - 不可拷贝或特殊键名:
Symbol类型的属性键及属性值处理、原型链继承状态保留,以及不可序列化的Function、DOM 节点、系统句柄等边界处置。
二、主流深拷贝方案的实现原理与实操代码
1. lodash.cloneDeep:成熟完备的工具库方案
lodash.cloneDeep 是 JavaScript 社区长期以来的深拷贝标杆。其内部通过 baseClone 算法驱动,具备高度严密的工程防御能力:
- 循环引用解决:内部维护了一个
Stack缓存栈,在浅层阶段使用数组缓存,当属性数量超过阈值时自动升级为哈希表MapCache,以线性时间复杂度检索是否已经克隆过该对象引用。 - 类型标签分发:通过
Object.prototype.toString.call()精准获取对象内在类型 Tag,针对 Date、RegExp、Map、Set、TypedArray、Buffer 等数十种内置类型分流至专门的初始化函数。 - 函数处理机制:对于函数(Function),
cloneDeep默认直接保留原引用,既不会抛出运行时异常,也不会尝试深复制闭包上下文(因为克隆闭包在通用 JS 环境中无法完美实现)。
// lodash.cloneDeep 典型使用示例
import cloneDeep from 'lodash/cloneDeep'; // 推荐按需引入
const original = {
name: 'Antigravity',
date: new Date('2026-10-31'),
reg: /^[a-z]+$/gi,
tags: new Set(['js', 'clone']),
scores: new Map([['math', 99]]),
action: () => console.log('active'),
};
// 构造循环引用
original.self = original;
const cloned = cloneDeep(original);
// 验证结果
console.log(cloned.date instanceof Date); // true
console.log(cloned.reg instanceof RegExp); // true
console.log(cloned.self === cloned); // true(正确指向克隆后自身)
console.log(cloned.self !== original); // true(与原对象完全隔离)
console.log(typeof cloned.action); // 'function'(安全保留函数引用)
import cloneDeep from 'lodash/cloneDeep' 按需引入,由于其内部依赖链涉及近 30 个辅助文件(如 isObject、getTag、Stack 等),打包解包体积通常仍在 15KB ~ 20KB 左右。
2. 原生 structuredClone():现代 Web 与 Node.js 内置标准
structuredClone() 是 HTML 规范中结构化克隆算法(The Structured Clone Algorithm)的直接暴露。目前已被 Chrome 98+、Firefox 94+、Safari 15.4+ 以及 Node.js 17.0.0+(以及 Deno、Bun 等现代运行时)作为全局原生函数广泛支持。
与 JS 层面的递归实现不同,structuredClone() 由底层引擎以 C++ 编写,执行过程不依赖任何外部 npm 包,零引入体积。
- 原生支持集合与特殊对象:直接克隆
Date、RegExp、Map、Set、ArrayBuffer、DataView、TypedArray,且保持实例类型与内容完全一致。 - 天然支持循环引用:底层算法自动追踪对象图引用关系,正确保留复杂循环与多处共享引用拓扑。
- 严格的不可克隆保护:当对象中包含
Function、DOM 节点、Symbol键值、WeakMap时,算法不会默默丢失或浅层复制,而是会明确抛出DataCloneError异常。
// 原生 structuredClone 使用示例
const state = {
session: 'token-xyz',
createdAt: new Date(),
pattern: /test/i,
metadata: new Map([['level', 'admin']]),
list: [1, 2, { deep: true }]
};
state.circular = state;
try {
const copy = structuredClone(state);
console.log(copy.createdAt instanceof Date); // true
console.log(copy.metadata.get('level')); // 'admin'
console.log(copy.circular === copy); // true
} catch (err) {
console.error('克隆失败:', err.name); // 若含有函数则抛出 DataCloneError
}
3. 经典 JSON.parse(JSON.stringify()):简易序列化的边界与硬伤
在 structuredClone 出现之前,很多轻量脚本喜欢使用 const clone = JSON.parse(JSON.stringify(obj))。这种方式代码极短、无需引入依赖,但其本质是 JSON 序列化协议而非对象拷贝工具,存在致命的类型失真缺陷:
- 直接丢失属性:属性值为
undefined、Function或Symbol时,在序列化后会被直接丢弃;若数组项包含它们,则会被强制转为null。 - 内置对象降级:
Date对象会被转换为 ISO-8601 字符串,反序列化后变成普通的string;RegExp和Error则会被直接降级为空对象{}。 - 数值异常被篡改:
NaN、Infinity、-Infinity会被强行序列化为null。 - 遇到循环引用直接崩溃:执行
JSON.stringify时一旦遇到循环引用,会直接抛出运行时错误TypeError: Converting circular structure to JSON。
4. 定制手写递归方案:精确掌控克隆逻辑
在特定的微型框架或独立 SDK 中,既不想增加 lodash 的依赖体积,又需要在某些缺少 structuredClone 的环境运行,通常可以实现一个基于 WeakMap 的轻量递归克隆函数:
// 兼顾循环引用与常用特殊类型的轻量递归深拷贝
function deepClone(target, hash = new WeakMap()) {
// 1. 处理基本类型与 null
if (target === null || typeof target !== 'object') {
return target;
}
// 2. 处理特殊内置对象
if (target instanceof Date) return new Date(target.getTime());
if (target instanceof RegExp) return new RegExp(target.source, target.flags);
if (target instanceof Set) {
const newSet = new Set();
hash.set(target, newSet);
target.forEach(val => newSet.add(deepClone(val, hash)));
return newSet;
}
if (target instanceof Map) {
const newMap = new Map();
hash.set(target, newMap);
target.forEach((val, key) => newMap.set(deepClone(key, hash), deepClone(val, hash)));
return newMap;
}
// 3. 循环引用直接读取缓存
if (hash.has(target)) {
return hash.get(target);
}
// 4. 初始化克隆目标(继承原对象原型)
const cloneTarget = Array.isArray(target) ? [] : Object.create(Object.getPrototypeOf(target));
hash.set(target, cloneTarget);
// 5. 使用 Reflect.ownKeys 兼顾常规属性与 Symbol 属性
Reflect.ownKeys(target).forEach(key => {
cloneTarget[key] = deepClone(target[key], hash);
});
return cloneTarget;
}
三、深拷贝方案的核心维度深度对比
1. 数据类型与特性支持全景
下表展示了四大方案在 JavaScript 各种复杂数据结构下的具体表现:
| 测试数据类型 / 语言特性 | lodash.cloneDeep | 原生 structuredClone | JSON.parse(JSON.stringify) | 手写递归 (WeakMap版) |
|---|---|---|---|---|
| 普通对象与数组 | √ 完全支持 | √ 完全支持 | √ 完全支持 | √ 完全支持 |
| 循环引用 (Circular Ref) | √ 完美保留拓扑 | √ 完美保留拓扑 | × 抛出 TypeError | √ 依赖 WeakMap 解决 |
| Date 日期实例 | √ 独立 Date 实例 | √ 独立 Date 实例 | ! 降级为字符串 | √ 单独分支构造 |
| RegExp 正则表达式 | √ 独立 RegExp 实例 | √ 独立 RegExp 实例 | × 变为空对象 {} | √ 单独分支构造 |
| Map / Set 集合 | √ 深度克隆集合元素 | √ 深度克隆集合元素 | × 变为空对象 {} | √ 递归处理集合键值 |
| 函数 (Function) | ! 浅层保留原函数引用 | × 抛出 DataCloneError | × 字段静默丢失 | ! 依实现决定保留/跳过 |
| Symbol 属性名与属性值 | √ 完整遍历克隆 | × 抛错或无法序列化 | × 字段静默丢失 | √ Reflect.ownKeys 支持 |
| ArrayBuffer / TypedArray | √ 深度复制缓冲区 | √ 深度复制缓冲区 | × 破坏为普通对象 | ! 需手动增加类型逻辑 |
| 原型链与继承状态 | ! 默认复制至普通原型 | × 丢弃原型(变为普通对象) | × 丢弃原型(Object.prototype) | √ Object.getPrototypeOf 可控 |
2. 打包体积与生态引入成本
在现代前端工程(如 Vite、Rollup、Webpack)打包体系中,包体积直接关系到首屏加载时长与内存占用:
- structuredClone():内置于浏览器内核或 V8 引擎底层,打包体积为 0 KB,不产生任何 node_modules 依赖。
- lodash.cloneDeep:完整 Lodash 引入约 70KB+(未 Gzip);单独引入
lodash.clonedeep经打包后依然占用约 15KB ~ 20KB。对于致力于极致轻量化的工具库或移动端首屏页面而言,这是一笔不可忽略的开销。 - JSON 方案:原生 JavaScript 内置方法,体积为 0 KB。
- 手写轻量函数:压缩后代码量通常仅在 0.5KB ~ 1.5KB 之间,无第三方依赖维护风险。
3. 运行性能与执行耗时分析
在实际执行效率上,深拷贝方案的性能表现与数据对象的规模和形态密切相关:
- 小对象 / 浅层数据结构(属性小于 10 个且无深层嵌套):手写递归或直接解构赋值耗时最短。
structuredClone()由于跨越了 JS 虚拟机与引擎 C++ 底层上下文的边界,具备一定的固定初始化成本(Micro-overhead),在毫秒微基准测试中小对象克隆速度可能略逊于精简的纯 JS 递归。 - 大对象 / 多层嵌套 / 包含万级 Map、Set、ArrayBuffer:
structuredClone()展现出碾压式的吞吐性能。其底层的内存块序列化直接在 C++ 层面完成,无需经过 V8 频繁的解释执行和密集的 JS 堆对象创建,大幅降低了垃圾回收器(Garbage Collection)的即时回收压力。 - JSON 序列化:尽管底层也是 C++ 实现,但必须经历“对象图遍历 → 序列化为 UTF-16 字符串 → 字符串语法词法解析 → 重建新对象”的两道工序,存在冗余的中间字符串内存分配,在复杂对象下整体效率并不占优。
四、工程实战避坑指南
1. 防御性包装 structuredClone 处理未知数据
在实际生产环境中,后端返回的复杂配置或第三方库传递的对象可能包含不可预知的函数回调或 Symbol。直接调用 structuredClone 触发未捕获的 DataCloneError 会导致业务线程崩溃。推荐在业务核心层编写降级包装:
// 生产环境安全深拷贝封装
export function safeStructuredClone(target) {
if (typeof structuredClone === 'function') {
try {
return structuredClone(target);
} catch (e) {
if (e.name === 'DataCloneError') {
// 数据包含函数或特殊对象时,降级采用 lodash 或手写拷贝
return fallbackClone(target);
}
throw e;
}
}
return fallbackClone(target);
}
2. 类实例(Class Instance)与原型链丢失
值得特别注意的是:structuredClone() 与 JSON 方案均不会克隆原型链。
如果一个对象是通过 class User { ... } 实例化的,使用 structuredClone(user) 克隆后,返回的新对象其原型将被重置为普通 Object.prototype,导致 copy instanceof User 为 false,所有挂载在原型上的方法也将无法调用。对于面向对象架构或充血模型(Domain Model)的深拷贝,必须使用手写递归保留原型链,或在类内部显式实现 user.clone() 实例方法。
3. Vue 3 响应式对象(Proxy)的拷贝陷阱
在 Vue 3 中,reactive() 返回的对象被底层 Proxy 所包裹。如果直接将响应式 Proxy 传入 structuredClone(),引擎可能会克隆出内部不可预知的代理状态,甚至导致响应式依赖追踪脱轨。
在拷贝 Vue 响应式状态前,务必使用 Vue 提供的 toRaw() 提取出底层的纯 JavaScript 原生对象,再进行深拷贝:
import { toRaw } from 'vue';
// 正确做法:先解包再克隆
const rawData = toRaw(reactiveState);
const clonedState = structuredClone(rawData);
五、选型总结与演进建议
综合功能完备性、包体积成本与现代规范演进,深拷贝方案的选型原则可以归纳为以下三条清晰结论:
- 首选原生
structuredClone():对于所有目标运行环境为现代浏览器或 Node.js 18+ 的新项目,只要待拷贝数据属于常规业务状态、集合数据且不含函数,structuredClone()是兼顾 0 打包体积、原生安全性和高性能的绝对首选。 - 复杂混合模型保留
lodash.cloneDeep:在历史老旧系统维护、需要支持 IE/旧版移动端 WebView、或者数据结构中深度混杂业务方法函数(且希望静默保留原函数引用)的场景中,lodash.cloneDeep依然是最具鲁棒性的工业级解决方案。 - 严防滥用
JSON 序列化:除最简单的纯字符串/数值键值对配置文件快照外,不要将JSON.parse(JSON.stringify())当作通用深拷贝使用,避免因 Date 变字符串、RegExp 变空对象或循环引用引发线上隐蔽缺陷。
JS处理含Emoji或中文的字符串转数组乱码问题
2026-11-05 16:12:28
JS怎么使用展开运算符合并对象
2026-11-05 14:42:18
ES6+环境下判断JS对象为空的简洁写法
2026-11-05 12:12:23
怎么通过replacer函数自定义JS中JSON的格式化输出
2026-11-05 11:19:44
JS正则表达式基础:字面量与RegExp构造函数的区别及常用元字符
2026-11-05 11:16:58
相关标签:







![JS输出对象显示[object Object]怎么办?JSON.stringify格式化打印技巧](https://img.javascript.cn/tit/articles/13236/20261105/6eaec9baed78171fe0bc2b74184ba5b7b80ebd52de7d3323ff152268aa2df294.webp)
