登录
首页 >  文章 >  前端

JavaScript深拷贝方法与避坑指南

时间:2026-02-22 19:41:42 242浏览 收藏

JavaScript深拷贝远非表面看起来那么简单,看似便捷的JSON序列化方案实则漏洞百出,会 silently 丢弃函数、undefined、Symbol、Date等关键类型并彻底崩溃于循环引用;尽管structuredClone()作为现代原生方案已大幅改善兼容性与健壮性,却仍对函数和Symbol无能为力;手写实现需精细识别各类内置对象并妥善处理循环引用缓存,稍有疏忽便导致栈溢出或类型失真;而广受信赖的Lodash _.cloneDeep()虽覆盖广泛,却明确不深拷贝函数体、不保留原型链、不调用自定义类构造器——每种方案都在能力与取舍间划出清晰边界,理解这些陷阱,才是写出可靠深拷贝逻辑的第一步。

javascript深拷贝如何实现_有哪些常见的方法与陷阱?

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() 支持 DateRegExpMapSetArrayBufferTypedArrayBigIntObjectArray 等,也支持循环引用,且保持原型链无关(即结果总是 plain object/array)。

使用注意点:

  • 不支持函数、undefinedSymbol —— 遇到会抛错: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' 会把 nullArrayDateRegExp 全部混为一谈。

关键判断逻辑应优先使用 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() 真的能覆盖所有场景?

它比手写健壮得多,支持函数(浅拷贝函数引用)、undefinedSymbolErrorPromiseDOM 节点等,也处理循环引用。但要注意两点:

  • 它不会深拷贝函数体,只是复制引用 —— 这是设计选择,不是 bug
  • 它不 clone prototype 上的属性,也不保留构造器,结果始终是 plain object/array/类数组
  • 体积较大(约 15KB minzipped),如果项目已用 Webpack/Rollup,建议按需引入:import cloneDeep from 'lodash-es/cloneDeep';

真正容易被忽略的是:当你依赖某个库内部用了 _.cloneDeep(),而你传入了自定义类实例(如 class User {}),它只会拷贝可枚举属性,不会调用 User 的 constructor 或 getter/setter —— 这不是缺陷,是通用工具的合理取舍。

以上就是本文的全部内容了,是否有顺利帮助你解决问题?若是能给你带来学习上的帮助,请大家多多支持golang学习网!更多关于文章的相关知识,也可关注golang学习网公众号。

资料下载
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>