A user_4a13383d 2026-09-04 09:01 4 次浏览 Javascript 深拷贝和浅拷贝 浅拷贝像只复印文件夹封面,深拷贝则是连里面的文件一起复印。真正容易出问题的地方,通常藏在嵌套对象、循环引用和特殊类型里。 本文只讲清概念、常见方案和选型,不重复手写递归实现。需要学习 `WeakMap`、循环引用和属性描述符的完整代码,请阅读 [[深拷贝代码实现](https://www.sanwan.xyz/posts/3)。 ## 目录 * [一、为什么会有浅拷贝和深拷贝](#%E4%B8%80%E4%B8%BA%E4%BB%80%E4%B9%88%E4%BC%9A%E6%9C%89%E6%B5%85%E6%8B%B7%E8%B4%9D%E5%92%8C%E6%B7%B1%E6%8B%B7%E8%B4%9D) * [二、浅拷贝:外层独立,里层可能共用](#%E4%BA%8C%E6%B5%85%E6%8B%B7%E8%B4%9D%E5%A4%96%E5%B1%82%E7%8B%AC%E7%AB%8B%E9%87%8C%E5%B1%82%E5%8F%AF%E8%83%BD%E5%85%B1%E7%94%A8) * [三、深拷贝:嵌套数据也各自独立](#%E4%B8%89%E6%B7%B1%E6%8B%B7%E8%B4%9D%E5%B5%8C%E5%A5%97%E6%95%B0%E6%8D%AE%E4%B9%9F%E5%90%84%E8%87%AA%E7%8B%AC%E7%AB%8B) * [四、常见深拷贝方案](#%E5%9B%9B%E5%B8%B8%E8%A7%81%E6%B7%B1%E6%8B%B7%E8%B4%9D%E6%96%B9%E6%A1%88) * [五、边界问题:不是所有对象都能随便复制](#%E4%BA%94%E8%BE%B9%E7%95%8C%E9%97%AE%E9%A2%98%E4%B8%8D%E6%98%AF%E6%89%80%E6%9C%89%E5%AF%B9%E8%B1%A1%E9%83%BD%E8%83%BD%E9%9A%8F%E4%BE%BF%E5%A4%8D%E5%88%B6) * [六、怎么选择](#%E5%85%AD%E6%80%8E%E4%B9%88%E9%80%89%E6%8B%A9) * [七、总结](#%E4%B8%83%E6%80%BB%E7%BB%93) ## 一、为什么会有浅拷贝和深拷贝 JavaScript 的对象变量保存的是一个引用,可以把它理解成“对象住址”。两个变量可能指向同一个对象: ```javascript const original = { name: 'Ada' }; const another = original; another.name = 'Grace'; console.log(original.name); // Grace ``` 这里没有复制对象,只是给同一个对象又起了一个名字。复制操作要解决的问题是:新对象是否要和原对象共享嵌套数据。 ## 二、浅拷贝:外层独立,里层可能共用 浅拷贝只复制第一层属性。常见写法有: ```javascript const source = { name: 'Ada', settings: { theme: 'dark', }, }; const copyByAssign = Object.assign({}, source); const copyBySpread = { ...source }; ``` 先看最容易踩的坑: ```javascript const source = { user: { name: 'Ada', }, }; const copy = { ...source }; copy.user.name = 'Grace'; console.log(source.user.name); // Grace console.log(copy.user === source.user); // true ``` 外层对象确实是新的,但 `user` 仍然是同一个引用。只要修改嵌套对象,原对象就会被一起改掉。 浅拷贝并不是“低级方案”。如果对象只有一层,或者你明确希望共享某些不可变数据,它反而简单、快速、语义清楚。问题在于:不要把浅拷贝误当成深拷贝。 ## 三、深拷贝:嵌套数据也各自独立 深拷贝会为嵌套对象创建新的实例: ```javascript const source = { user: { name: 'Ada', }, }; const copy = structuredClone(source); copy.user.name = 'Grace'; console.log(source.user.name); // Ada console.log(copy.user === source.user); // false ``` 这次修改副本不会影响原对象,因为两份 `user` 已经分开。 不过,“深拷贝”不是一个保证什么都能复制的魔法。不同实现支持的类型不同,复制后是否保留原型、函数、访问器和外部资源,也需要单独确认。 ## 四、常见深拷贝方案 ### 1. `JSON.parse(JSON.stringify())` 这是最容易想到的方案,适合结构简单、明确就是 JSON 数据的对象: ```javascript const source = { name: 'Ada', createdAt: new Date('2024-01-01'), optional: undefined, }; const copy = JSON.parse(JSON.stringify(source)); console.log(copy); // { // name: 'Ada', // createdAt: '2024-01-01T00:00:00.000Z' // } ``` 它的限制也很明确: * 循环引用会直接抛出异常。 * `undefined`、函数和 `Symbol` 属性可能被忽略。 * `Date` 会变成字符串。 * `Map`、`Set`、`RegExp` 等类型无法按原类型保留。 * `NaN`、`Infinity` 等值会发生变化。 所以它不是通用深拷贝,只是“把数据转成 JSON,再转回来”。 ### 2. `structuredClone` 现代浏览器和 Node.js 提供的原生方案: ```javascript const source = { map: new Map([['name', 'Ada']]), set: new Set([1, 2, 3]), }; const copy = structuredClone(source); console.log(copy.map instanceof Map); // true console.log(copy.set instanceof Set); // true ``` 它支持循环引用,也支持许多常见内置类型。需要注意: * 函数不能被克隆,会抛出 `DataCloneError`。 * DOM 节点、带外部资源的对象和部分宿主对象不能直接克隆。 * 自定义类实例的原型和方法不要默认视为会完整保留,使用前要查清运行环境的行为。 * `structuredClone` 解决的是“复制数据”,不是“复制运行中的对象状态”。 ### 3\. 第三方库 工程项目中也可以使用 Lodash 的 `_.cloneDeep()` 等成熟实现: ```javascript import cloneDeep from 'lodash/cloneDeep'; const copy = cloneDeep(source); ``` 第三方库通常会覆盖更多常见类型和兼容性场景,但需要引入依赖、关注包体积,并阅读它对特殊对象的具体处理规则。 ## 五、边界问题:不是所有对象都能随便复制 ### 1\. 循环引用 对象可以指向自己: ```javascript const source = {}; source.self = source; // JSON.stringify(source); // TypeError: Converting circular structure to JSON ``` 这类数据不能使用 JSON 方案。`structuredClone` 可以处理很多循环引用;需要自己控制复制规则时,再阅读 [深拷贝代码实现.md](./%E6%B7%B1%E6%8B%B7%E8%B4%9D%E4%BB%A3%E7%A0%81%E5%AE%9E%E7%8E%B0.md)。 ### 2\. 函数和闭包 函数不仅是一段代码,还依赖它创建时的闭包环境。复制一个函数,并不能复制它捕获的变量,因此多数深拷贝方案会保留函数引用或直接报错: ```javascript const source = { format() { return 'hello'; }, }; // 不要假设深拷贝方案能复制 format 的闭包和执行环境 ``` ### 3\. 原型和属性描述符 普通赋值通常只复制可枚举数据属性,不会保留完整的原型链、只读规则、访问器和不可枚举属性。需要这些语义时,要先明确复制目标: * 只要业务数据:选择能满足数据类型要求的方案。 * 需要原型和属性规则:使用专门实现,或阅读 [深拷贝代码实现.md](./%E6%B7%B1%E6%8B%B7%E8%B4%9D%E4%BB%A3%E7%A0%81%E5%AE%9E%E7%8E%B0.md)。 * 有自定义类实例:确认方法、私有字段和外部资源是否需要重新构造。 ### 4\. 性能和数据规模 深拷贝会遍历并创建大量对象,数据越大,时间和内存成本越高。不要为了“保险”对所有状态都深拷贝;如果只修改少量字段,可以使用结构共享和局部复制: ```javascript const nextState = { ...state, user: { ...state.user, name: 'Grace', }, }; ``` ## 六、怎么选择 | 实现方案 | 循环引用 | 复杂类型(Date/正则等) | 函数拷贝 | 适用场景 | | ---- | ---- | -------------- | ---- | ---- | | `JSON.parse` | ❌ 报错 | ❌ 丢失或变异 | ❌ 忽略 | 简单的业务数据对象克隆 | | `structuredClone` | ✅ 支持 | ✅ 大部分支持 | ❌ 报错 | 现代浏览器环境下的首选方案 | | Lodash `cloneDeep` | ✅ 支持 | ✅ 支持 | ⚠️ 不拷贝(保留引用) | 工程化项目、对兼容性和健壮性要求极高 | | 手写递归深拷贝 | ✅ 需借助 `WeakMap` | ✅ 需单独写判断逻辑 | ✅ 可自定义处理 | 面试手撕代码、需极度定制化的场景 | ## 七、总结 * 浅拷贝只复制第一层,嵌套对象仍然可能共享引用。 * 深拷贝的重点不是“复制层数”,而是确认引用关系和类型语义是否符合需求。 * JSON 转换简单,但会丢失类型、无法处理循环引用。 * `structuredClone` 是现代环境下的优先选择,但函数、宿主对象和自定义实例仍要单独确认。 * 深拷贝会增加时间和内存成本,能做局部复制时不要全量复制。 * 需要学习手写递归、`WeakMap` 和属性描述符时,请阅读 [深拷贝代码实现]。 本文不再附加手写递归案例,避免与实现篇重复。 点赞0 收藏0
评论
还没有评论,来分享你的想法吧。