JSON.parse(JSON.stringify(obj))不能当深拷贝用,因会丢弃函数、undefined、Symbol等,且无法处理循环引用;structuredClone()是目前最靠谱的原生方案,但不支持函数、undefined、Symbol;手写需注意特殊类型识别与循环引用缓存;Lodash的_.cloneDeep()覆盖广但不深拷贝函数体、不保留原型。

JSON.parse(JSON.stringify(obj)) 为什么不能当深拷贝用?
它看起来最简单,但实际只适合纯数据对象——没有函数、undefined、Symbol、Date、RegExp、Map、Set、BigInt,也不能处理循环引用。JSON.stringify() 遇到这些值会静默丢弃或报错,比如:undefined 和函数直接被忽略,Date 变成字符串,NaN 变成 null。
常见错误现象:
-
JSON.parse(JSON.stringify({ time: new Date() }))→{ time: "2024-01-01T00:00:00.000Z" }(不再是Date实例) -
JSON.parse(JSON.stringify({ fn() {} }))→{}(函数消失) -
const obj = {}; obj.self = obj; JSON.stringify(obj)→ 报错TypeError: Converting circular structure to JSON
structuredClone() 是目前最靠谱的原生方案吗?
是的,但它有明确的浏览器兼容性边界和类型限制。structuredClone() 支持 Date、RegExp、Map、Set、ArrayBuffer、TypedArray、BigInt、Object、Array 等,也支持循环引用,且保持原型链无关(即结果总是 plain object/array)。
使用注意点:
立即学习“Java免费学习笔记(深入)”;
- 不支持函数、
undefined、Symbol—— 遇到会抛错:DataCloneError: function is not supported - Node.js 17+ 默认启用,但需开启
--enable-structured-cloning标志;Node.js 18.13+ 和 20.6+ 已默认启用 - 浏览器中 Chrome 98+、Firefox 94+、Safari 16.4+ 支持;IE 完全不支持
const original = { date: new Date(), map: new Map([['a', 1]]), self: null };
original.self = original;
const cloned = structuredClone(original); // ✅ 成功,cloned.self 指向 cloned 自身
手写递归深拷贝时最容易漏掉什么?
不是“递归调用”本身,而是对特殊内置对象和边界类型的识别与分发。比如只判断 typeof obj === 'object' 会把 null、Array、Date、RegExp 全部混为一谈。
关键判断逻辑应优先使用 Object.prototype.toString.call():
-
[object Array]→ 用map+ 递归 -
[object Date]→new Date(obj.getTime()) -
[object RegExp]→new RegExp(obj)(注意 flags) -
[object Map]→new Map([...obj].map(([k, v]) => [k, deepClone(v)])) -
null要单独处理,否则Object.keys(null)报错 - 循环引用必须缓存已拷贝的源对象映射,否则栈溢出
function deepClone(obj, seen = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (seen.has(obj)) return seen.get(obj);
const tag = Object.prototype.toString.call(obj);
let cloned;
if (tag === '[object Array]') {
cloned = [];
} else if (tag === '[object Date]') {
cloned = new Date(obj.getTime());
} else if (tag === '[object RegExp]') {
cloned = new RegExp(obj.source, obj.flags);
} else {
cloned = {};
}
seen.set(obj, cloned);
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
cloned[key] = deepClone(obj[key], seen);
}
}
return cloned;
}
Lodash 的 _.cloneDeep() 真的能覆盖所有场景?
它比手写健壮得多,支持函数(浅拷贝函数引用)、undefined、Symbol、Error、Promise、DOM 节点等,也处理循环引用。但要注意两点:
- 它不会深拷贝函数体,只是复制引用 —— 这是设计选择,不是 bug
- 它不 clone
prototype上的属性,也不保留构造器,结果始终是 plain object/array/类数组 - 体积较大(约 15KB minzipped),如果项目已用 Webpack/Rollup,建议按需引入:
import cloneDeep from 'lodash-es/cloneDeep';
真正容易被忽略的是:当你依赖某个库内部用了 _.cloneDeep(),而你传入了自定义类实例(如 class User {}),它只会拷贝可枚举属性,不会调用 User 的 constructor 或 getter/setter —— 这不是缺陷,是通用工具的合理取舍。











