
引言在 JS 里复制一个对象看起来简单做起来处处是坑。你以为是复制了一份独立的数据结果改了新对象原对象也跟着变。或者你小心翼翼地复制了一层结果嵌套里面的对象还是共享的。问题就出在浅拷贝和深拷贝的区别上。这篇文章只讲这一个问题把复制的逻辑讲清楚。一、问题的根源引用类型要理解拷贝先要理解引用类型。原始类型的变量存的是值本身。复制一个数字变量得到的是独立的两份值改一个不影响另一个。引用类型的变量存的是地址。复制一个对象变量复制的只是地址两个变量指向同一个物体。改其中一个另一个看到的也是改后的结果。所以复制对象不能简单赋值。必须创建新对象把原对象的内容搬过去。但搬到什么程度就是浅拷贝和深拷贝的分界线。二、浅拷贝只复制第一层浅拷贝顾名思义只复制对象的第一层。它创建一个新对象把原对象第一层的属性值复制过来。如果属性值是原始类型复制的是值互不影响。如果属性值是引用类型复制的是地址新对象和原对象仍然共享那个嵌套对象。所以浅拷贝之后改新对象的第一层原始属性原对象不受影响但改新对象里嵌套的对象原对象也会变。浅拷贝的实现方式有多种展开运算符、对象合并方法、数组切片方法等本质上都是只处理第一层。三、深拷贝递归复制所有层深拷贝是要得到一个完全独立的新对象无论嵌套多少层都不共享。它的思路是递归遇到原始类型直接复制值遇到引用类型就新建一个同类型容器再递归复制里面的内容。一层层往下直到所有层级都复制完。深拷贝之后改新对象的任何地方原对象都不会受影响。两个对象彻底独立。四、深拷贝的难点深拷贝听起来简单实际实现要考虑很多情况。第一循环引用。对象 A 引用了 BB 又引用了 A。递归复制时会无限循环导致栈溢出。必须记录已经复制过的对象遇到重复的直接复用才能打断循环。第二特殊类型。日期、正则、映射、集合这些类型不能简单当作普通对象处理需要分别判断并特殊处理否则复制出来的东西会丢失原有特性。第三函数。函数也是对象但通常不复制函数体而是直接共享引用。深拷贝一般只处理数据不处理行为。第四原型链。简单递归会丢失原对象的原型复制出来的对象可能不再是原来的类型。需要额外处理原型关系。第五性能。深拷贝要遍历所有层级对象越大越慢。对大对象做深拷贝可能造成明显卡顿。五、常用的深拷贝方式及其局限手动递归是最可控的方式可以根据需求处理各种特殊情况但代码量大容易漏掉边界情况。序列化再反序列化是一种取巧方式把对象转成字符串再转回对象。简单但有明显局限会丢失函数、日期会变成字符串、映射和集合会丢失、循环引用直接报错。只适合纯数据且结构简单的场景。结构化克隆是较新的浏览器能力能处理更多类型也支持循环引用但并非所有环境都支持且不能复制函数。第三方库提供了成熟的深拷贝实现考虑了大量边界情况但引入依赖需要权衡体积。没有一种方式完美适用于所有场景选择取决于数据类型和运行环境。六、什么时候用浅拷贝什么时候用深拷贝浅拷贝够用的场景对象只有一层或者嵌套部分本来就不需要独立你明确知道哪些部分共享、哪些部分独立性能敏感不想付出深拷贝的代价。必须深拷贝的场景需要完全独立的数据副本修改副本不能影响原数据对象结构复杂且嵌套深要把数据传给不可信的外部逻辑防止被意外修改。很多时候浅拷贝加手动处理特定嵌套字段比无脑深拷贝更高效。不是越深越好而是按需复制。七、不可变思维现代前端开发中越来越流行不可变的思路不修改原对象每次变化都产生新对象。这种思路下浅拷贝用得非常频繁。因为只要保证每次修改都创建新对象不直接改原对象就能实现状态的可追踪。对于嵌套结构可以只复制变化路径上的节点其余部分共享既保证独立性又节省性能。理解浅拷贝和深拷贝是掌握这种思维的基础。八、总结浅拷贝只复制第一层嵌套的引用仍然共享深拷贝递归复制所有层得到完全独立的对象。深拷贝要处理循环引用、特殊类型、性能等问题没有一种方式放之四海而皆准。实际开发中要根据数据结构和需求选择复制深度而不是一律深拷贝。理解这个区别你就能避免改了副本原对象也变的坑也能在性能敏感时做出合理取舍。