JavaScript 深拷贝与浅拷贝:从一张纸条说起

一、变量里装的到底是什么?

当你写 let a = { x: 1 } 时,a 里装的不是那个对象,而是一张纸条——纸条上写着地址。对象本身躺在内存里,纸条告诉你"去这个地址找它"。

基本类型(numberstringboolean)不一样,值直接写在纸条上。但对象、数组、函数,纸条上永远只是个地址。

搞清楚这一点,剩下的全是推论。

二、赋值不是拷贝

let a = { x: 1 };
let b = a;        // 抄了纸条,没抄房子
b.x = 999;
console.log(a.x); // 999 —— 同一栋房子

ab 是两张纸条,写着同一个门牌号。改 b 就是改 a。这不叫拷贝。

三、浅拷贝:抄了第一层,嵌套的还是共享

let a = { x: 1, nested: { y: 2 } };
let b = { ...a };

b.nested.y = 999;
console.log(a.nested.y); // 999 —— 嵌套层还是同一栋

展开运算符 ...a 建了一栋新房子,但 nested 这个抽屉里放的还是原来那张地址条。浅拷贝只抄了一层深,再往下的引用还是共享的。

四、深拷贝:一路抄到底

遇到引用就顺着走过去,把那栋房子也抄一份,把新地址条放进新房子。一路递归到底,直到所有东西都是新建的,两份完全独立。

听起来简单,做起来全是坑。自己造一个就知道了:

function deepClone(source, cache = new WeakMap()) {
  if (source === null || typeof source !== 'object') return source;
  if (typeof source === 'function') return source;
  if (cache.has(source)) return cache.get(source); // 防循环引用

  if (source instanceof Date) return new Date(source.getTime());
  if (source instanceof RegExp) return new RegExp(source.source, source.flags);

  const target = Array.isArray(source) ? [] : {};
  cache.set(source, target); // 先记录,再递归

  Reflect.ownKeys(source).forEach(key => {
    target[key] = deepClone(source[key], cache);
  });
  return target;
}

这还没完——Map/Set 要特殊处理,getter/setter 会变成普通属性,原型链用 {} 会丢失。深拷贝没有"完美"实现,每补一个坑就发现下一个。

五、现成方案对比

赋值 浅拷贝 JSON方案 structuredClone _.cloneDeep
第一层独立
嵌套层独立
循环引用 ✗(报错)
函数 共享 共享 丢失 ✗(报错) 共享
Date/RegExp 共享 共享 ✗(变字符串)
Map/Set 共享 共享 ✗(丢失)
原型链 保留 保留 ✗(丢失) ✗(丢失) 保留
  • JSON.parse(JSON.stringify()):只适合纯 JSON 数据,循环引用直接崩。
  • structuredClone():日常首选,支持循环引用和大部分内置类型,但函数会报错、原型链不保留。
  • _.cloneDeep():最全面,函数不报错直接共享,保留原型链。

读这张表的方法:先问"我需要什么",再选方案。 而不是选一个方案然后祈祷它覆盖所有情况。

六、收尾

深拷贝之所以存在,是因为 JS 对象是引用类型——变量里装的是纸条,不是房子。一旦你真正理解了这一点,浅拷贝和深拷贝的行为就是推论,不是需要背的知识点。

知道 structuredClone 这个名字,不代表你理解它为什么拒绝拷贝函数。真正理解的人,能从纸条和房子的关系开始,把整个逻辑链重新推导出来。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容