JSON.parse(JSON.stringify(obj))不能当深拷贝用,因会丢弃函数、undefined、Symbol等,且无法处理循环引用;structuredClone()是目前最靠谱的原生方案,但不支持函数、undefined、Symbol;手写需注意特殊类型识别与循环引用缓存;Lodash的_.cloneDeep()覆盖广但不深拷贝函数体、不保留原型。
它看起来最简单,但实际只适合纯数据对象——没有函数、undefined、Symbol、Date、RegExp、Map、Set、BigInt,也不能处理循环引用。JSON.stringify() 遇到这些值会静默丢弃或报错,比如:undefined 和函数直接被忽略,Date 变成字符串,NaN 变成 null。
常见错误现象:
JSON.parse(JSON.stringify({ time: new Date() })) → { time: "2025-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() 支持 Date、RegExp、Map、Set、ArrayBuffer、TypedArray、BigInt、Object、Array 等,也支持循环引用,且保持原型链无关(即结果总是 plain object/array)。
使用注意点:
undefined、Symbol —— 遇到会抛错:DataCloneError: function is not supported
--enable-structured-cloning 标志;Node.js 18.13+ 和 20.6+ 已默认启用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;
}
它比手写健壮得多,支持函数(浅拷贝函数引用)、undefined、Symbol、Error、Promise、DOM 节点等,也处理循环引用。但要注意两点:
prototype 上的属性,也不保留构造器,结果始终是 plain object/array/类数组import cloneDeep from 'lodash-es/cloneDeep';
真正容易被忽略的是:当你依赖某个库内部用了 _.cloneDeep(),而你传入了自定义类实例(如 class User {}),它只会拷贝可枚举属性,不会调用 User 的 constructor 或 getter/setter —— 这不是缺陷,是通用工具的合理取舍。