JavaScript无完美深拷贝方案:JSON.parse(JSON.stringify())丢函数/undefined/Symbol/Date/RegExp/Map/Set且不支持循环引用;structuredClone()支持现代环境的多数类型但不支持函数/undefined/Symbol;手写递归需处理循环引用和类型判断;Lodash cloneDeep功能全面但体积大且5.0将移除。

JavaScript 没有原生的、完美通用的深拷贝函数,JSON.parse(JSON.stringify(obj)) 看似简单,但会丢函数、undefined、Symbol、Date、RegExp、Map、Set、循环引用等;直接用结构赋值或 Object.assign() 只是浅拷贝。
为什么 JSON.parse(JSON.stringify()) 不可靠
它本质是“序列化再反序列化”,只处理可 JSON 化的值:
-
function、undefined、Symbol会被忽略或转成null -
Date变成字符串,RegExp变成空对象{} -
Map/Set直接丢失(JSON 不支持) - 遇到循环引用会抛错:
TypeError: Converting circular structure to JSON - 原型链、getter/setter 全部丢失
示例:
const obj = { d: new Date(), r: /abc/, m: new Map([[1, 'a']]) };
JSON.parse(JSON.stringify(obj)); // { d: "2024-01-01T00:00:00.000Z", r: {}, m: undefined }
用 structuredClone()(现代浏览器首选)
这是目前最接近“标准深拷贝”的方案,支持 Date、RegExp、Map、Set、ArrayBuffer、TypedArray、BigInt 和循环引用,且保留类型语义。
- 仅限浏览器(Chrome 98+、Firefox 94+、Edge 98+)和 Node.js 17.0+(需启用
--experimental-structured-cloning,Node.js 18.15+ 默认开启) - 不支持函数、
undefined、Symbol—— 这是设计限制,不是 bug - 不能拷贝带有不可枚举属性或自定义 getter 的对象(只处理可枚举 + 可克隆的数据)
用法很简单:
const copy = structuredClone(originalObj);
立即学习“Java免费学习笔记(深入)”;
手写递归深拷贝(兼容性要求高时)
适用于需要支持老环境(如 IE)、或需定制行为(比如跳过某些字段、处理特殊类实例)的场景。关键点:
- 必须检测循环引用,用
WeakMap缓存已拷贝的源对象 - 区分基本类型、
null、数组、普通对象、Date、RegExp、Map、Set等,分别处理 - 避免用
typeof判数组(返回"object"),改用Array.isArray() - 不要用
for...in遍历对象 —— 它会遍历原型链;应使用Object.keys()或Object.getOwnPropertyNames()
最小可用骨架示例:
function deepClone(obj, seen = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (seen.has(obj)) return seen.get(obj);
let clone;
if (obj instanceof Date) clone = new Date(obj);
else if (obj instanceof RegExp) clone = new RegExp(obj);
else if (Array.isArray(obj)) clone = obj.map(item => deepClone(item, seen));
else if (obj instanceof Map) clone = new Map([...obj].map(([k, v]) => [k, deepClone(v, seen)]));
else if (obj instanceof Set) clone = new Set([...obj].map(v => deepClone(v, seen)));
else {
clone = Object.create(Object.getPrototypeOf(obj));
seen.set(obj, clone);
for (const key of Object.getOwnPropertyNames(obj)) {
const desc = Object.getOwnPropertyDescriptor(obj, key);
Object.defineProperty(clone, key, {
value: deepClone(desc.value, seen),
enumerable: desc.enumerable,
writable: desc.writable,
configurable: desc.configurable
});
}
}
return clone;
}
Lodash 的 cloneDeep() 是什么定位
它不是“银弹”,而是权衡后的实用方案:
- 覆盖绝大多数常见类型(包括函数、
undefined、Symbol、Error、Promise等),但对自定义类实例默认只做浅拷贝(除非你显式定义clone方法) - 内部做了大量边界处理(如稀疏数组、负索引、不可枚举属性),比手写更健壮
- 体积不小(约 20KB minified),如果项目已用 Lodash 且其他功能也在用,值得引入;否则为单个功能引入略重
- 注意:Lodash 5 将移除
cloneDeep,推荐迁移到lodash-es的按需导入或转向structuredClone
用法:
import { cloneDeep } from 'lodash-es';
const copy = cloneDeep(originalObj);
真正要选哪种方式,取决于你的运行环境、数据结构复杂度、是否允许丢弃某些值(比如函数),以及是否愿意承担额外依赖。别只看“能不能拷”,得看“拷完还是不是原来那个东西”。











