JS拷贝本质是理解内存模型:原始类型存栈中值,赋值即复制;引用类型变量存堆地址,赋值即共享。浅拷贝仅隔离第一层,嵌套仍共用;深拷贝需递归+类型识别+循环检测,structuredClone为现代首选。

学 JS 浅拷贝与深拷贝,关键不是堆砌方法,而是建立“数据在哪、怎么连、改了谁会变”的直觉。高级编程阶段,重点已从“会不会用”转向“为什么这么用”“换种场景还稳不稳”。下面分四块说透。
一、先盯住内存模型:栈、堆、值 vs 地址
所有拷贝行为的根源就在这里:
- 原始类型(string/number/boolean/null/undefined/Symbol/BigInt)存在栈里,变量存的是真实值;赋值 = 复制一份新数据,彼此绝缘
- 引用类型(Object/Array/Date/Map/Set/RegExp/Function)的变量存的是地址,真实内容在堆里;赋值 = 复制地址,两个变量共用同一块堆内存
- 所谓“拷贝失败”,本质是误把“配钥匙”当成“盖新房”——你拿到的是同一扇门的第二把钥匙,开门后改的还是同一个房间
二、浅拷贝只保第一层独立,嵌套仍是共享
它创建新对象,但只遍历顶层属性:
- 遇到原始值(如 name: '张三'),真复制一份,安全
- 遇到引用值(如 address: { city: '杭州' }),只把地址复制过去,新旧对象的 address 指向同一子对象
- 验证很简单:改 copy.address.city,再看 original.address.city 是否跟着变——变就是浅,不变才可能深
常用写法:{ ...obj }、Object.assign({}, obj)、[...arr]、arr.slice()、Array.from(arr)。它们都不处理原型链、不可枚举属性,对 null/undefined 入参行为也不统一。
三、深拷贝要彻底断开所有连接,包括循环引用
不是“递归就够了”,而是“递归 + 类型识别 + 循环检测”三位一体:
- JSON.parse(JSON.stringify(obj)) 是伪深拷贝:快但丢 function、undefined、Symbol、Date(转成字符串)、RegExp、Map/Set,遇循环引用直接报错
- structuredClone(obj) 是现代首选:原生支持 Date、RegExp、Map、Set、ArrayBuffer 和循环引用,Chrome 98+ / Firefox 94+ / Safari 15.4+ 已稳定可用
- 兼容老环境必须手写或用库:lodash.cloneDeep 内部用 WeakMap 记录已克隆对象,避免无限递归;自己实现也要加这层保护,否则一碰循环引用就栈溢出
四、工程中别硬背方案,用三问快速决策
写代码前花两秒自问:
- 数据结构是否扁平?全是基础类型或一层对象 → 浅拷贝足够,用展开运算符最轻量
- 有没有嵌套?下游会不会改深层字段?→ 必须深拷贝,不能再赌
- 目标环境是否支持 structuredClone?要不要兼容 IE 或旧安卓 WebView?→ 支持就用原生,不支持就上 cloneDeep 或封装带 WeakMap 的递归函数
React/Vue 中更新 state 或 props,更推荐用 immer 这类不可变更新工具,而不是手动拷贝——它底层自动做深拷贝+路径优化,语义清晰且性能可控。











