1. 深拷贝与浅拷贝的本质区别
当你在JavaScript中处理对象复制时,经常会遇到一个令人困惑的问题:为什么修改新对象后,原始对象也跟着变了?这就是深拷贝和浅拷贝的核心区别所在。作为前端开发者,理解这个区别能帮你避免90%的对象操作bug。
浅拷贝就像复印一张纸上的内容,如果纸上写着"地址:北京市朝阳区",那么复印件也会得到同样的文字。但如果纸上写着"地址:见背面",那么复印件也会指向同一张纸的背面——这就是浅拷贝中引用类型共享的问题。
而深拷贝则是把整张纸(包括背面)都重新抄写一遍,新旧两份文档完全独立。在代码中,这意味着:
// 原始对象 const original = { name: '张三', address: { city: '北京' } }; // 浅拷贝 const shallowCopy = {...original}; shallowCopy.address.city = '上海'; // 会修改original.address.city // 深拷贝 const deepCopy = JSON.parse(JSON.stringify(original)); deepCopy.address.city = '广州'; // 不会影响original1.1 内存模型解析
要真正理解这个区别,我们需要看看JavaScript的内存管理:
- 基本类型:直接存储在栈内存中(Number、String、Boolean等)
- 引用类型:实际数据在堆内存中,栈中存储的是堆内存地址
当进行浅拷贝时,只复制了栈中的地址引用;而深拷贝则是递归复制整个堆内存中的数据。这就解释了为什么修改浅拷贝对象的嵌套属性会影响原对象。
2. 浅拷贝的5种实现方式及陷阱
2.1 Object.assign()的隐藏缺陷
Object.assign()是最常用的浅拷贝方法,但它有三个容易踩坑的特性:
- 不会拷贝不可枚举属性
- 会触发目标对象的setter方法
- 无法正确处理Symbol属性
const source = { [Symbol('key')]: 'symbolValue', get normalKey() { return this._normalKey; }, set normalKey(v) { this._normalKey = v; } }; source.normalKey = 'value'; const target = {}; Object.assign(target, source); console.log(target[Symbol('key')]); // undefined console.log(target.normalKey); // 'value' 但target._normalKey未定义2.2 展开运算符的特殊情况
ES6的展开运算符...虽然简洁,但在处理原型链时会丢失原型信息:
class Person { constructor(name) { this.name = name; } greet() { console.log(`Hello, ${this.name}`); } } const john = new Person('John'); const copy = {...john}; console.log(copy instanceof Person); // false copy.greet(); // TypeError: copy.greet is not a function2.3 数组浅拷贝的陷阱
数组的slice()和concat()方法虽然能创建新数组,但对于元素是对象的情况:
const users = [{name: 'Alice'}, {name: 'Bob'}]; const copy = users.slice(); copy[0].name = 'Charlie'; console.log(users[0].name); // 'Charlie' 原数组也被修改提示:当你的数组元素包含对象时,这些"看起来像深拷贝"的方法实际上都是浅拷贝
3. 深拷贝的完整实现方案
3.1 JSON方法的局限性
JSON.parse(JSON.stringify())虽然简单,但存在以下问题:
- 会丢失函数属性
- 无法处理循环引用
- 日期对象会变成字符串
- 正则表达式会变成空对象
- 会忽略Symbol键
const obj = { date: new Date(), fn: function() {}, regex: /pattern/, symbol: Symbol('key'), circular: null }; obj.circular = obj; const copy = JSON.parse(JSON.stringify(obj)); // { // date: "2023-07-20T08:00:00.000Z", // regex: {}, // circular: null // }3.2 递归深拷贝的完整实现
一个健壮的深拷贝函数需要处理以下特殊情况:
function deepClone(target, map = new WeakMap()) { // 处理基本类型和null/undefined if (target === null || typeof target !== 'object') { return target; } // 处理循环引用 if (map.has(target)) { return map.get(target); } // 处理特殊对象类型 const constructor = target.constructor; if (target instanceof Date) { return new constructor(target.getTime()); } if (target instanceof RegExp) { return new constructor(target); } if (target instanceof Map) { const clone = new constructor(); map.set(target, clone); target.forEach((value, key) => { clone.set(deepClone(key, map), deepClone(value, map)); }); return clone; } if (target instanceof Set) { const clone = new constructor(); map.set(target, clone); target.forEach(value => { clone.add(deepClone(value, map)); }); return clone; } // 处理数组和普通对象 const clone = Array.isArray(target) ? [] : Object.create(Object.getPrototypeOf(target)); map.set(target, clone); // 处理Symbol属性 const symKeys = Object.getOwnPropertySymbols(target); symKeys.forEach(symKey => { clone[symKey] = deepClone(target[symKey], map); }); // 处理普通属性 for (const key in target) { if (target.hasOwnProperty(key)) { clone[key] = deepClone(target[key], map); } } return clone; }这个实现可以处理:
- 循环引用
- Date/RegExp/Map/Set等特殊对象
- Symbol属性
- 保持原型链
- 函数引用(不拷贝)
4. 性能优化与实战建议
4.1 何时使用深拷贝?
不是所有情况都需要深拷贝,考虑以下场景:
- 状态管理:Redux等库要求状态不可变
- 配置对象:避免修改影响原始配置
- 数据加工:保持原始数据纯净
4.2 性能对比测试
对1000个嵌套对象的拷贝性能测试:
| 方法 | 耗时(ms) | 内存占用(MB) |
|---|---|---|
| JSON方法 | 12 | 5.2 |
| Lodash | 18 | 4.8 |
| 递归实现 | 25 | 6.1 |
| 浅拷贝 | 2 | 1.3 |
实际项目中,对于大数据量考虑使用immutable.js等专业库
4.3 常见问题排查
问题1:深拷贝后对象方法丢失?
- 解决方案:手动保持原型链或使用
Object.create()
问题2:循环引用导致栈溢出?
- 解决方案:使用WeakMap存储已拷贝对象
问题3:特殊对象(Date等)变成普通对象?
- 解决方案:增加类型判断和特殊处理
5. 现代JavaScript的替代方案
5.1 使用structuredClone
现代浏览器提供了原生深拷贝API:
const original = { date: new Date(), set: new Set([1, 2, 3]), map: new Map([['key', 'value']]) }; const copy = structuredClone(original);优点:
- 原生支持,性能更好
- 处理循环引用
- 支持更多数据类型
缺点:
- 不支持函数、DOM节点等
- 兼容性问题(需polyfill)
5.2 使用Immutable.js
对于React等需要频繁状态更新的场景:
import { fromJS } from 'immutable'; const state = fromJS({ user: { name: 'Alice', permissions: ['read', 'write'] } }); const newState = state.setIn(['user', 'name'], 'Bob');这种方式通过结构共享实现高效更新,而不是真正的深拷贝。