从 JS 内存模型,彻底搞懂深拷贝与浅拷贝
在 JavaScript 开发中,你一定遇到过修改一个数组 / 对象后,另一个看似无关的变量也跟着变化的诡异情况。这背后其实不是 JS 的 bug,而是我们对栈内存和堆内存的存储规则、以及深 / 浅拷贝的本质理解不到位。今天就从内存模型的底层逻辑出发,把深拷贝这件事讲透。
一、先搞懂:JS 变量到底存在哪?
JS 中的变量存储分为两种区域,这是理解拷贝的核心:
1. 栈内存(Stack)
- 存储:简单数据类型(Number、String、Boolean、Undefined、Null、Symbol)
- 特点:空间小、连续存储、读写速度快,变量直接存储值本身
- 赋值逻辑:值拷贝(相当于复印文件,原文件和复印件互不影响)
// 栈内存示例:值拷贝
let a = 10;
let b = a; // 把a的值"复印"一份给b
b = 20; // 只修改b,a不受影响
console.log(a); // 10
console.log(b); // 20
2. 堆内存(Heap)
- 存储:复杂数据类型(Object、Array、Function)
- 特点:空间大、非连续存储,栈内存中只存堆内存地址(引用) ,真正的数据存在堆里
- 赋值逻辑:引用拷贝(相当于给文件建快捷方式,多个快捷方式指向同一个文件)
// 堆内存示例:引用拷贝(浅拷贝)
const users = [
{ id: 1, name: "张三", city: "北京" },
{ id: 2, name: "李四", city: "上海" }
];
const data = users; // 只是把users的内存地址赋值给data
data[0].hobby = ["读书", "跑步"]; // 修改data指向的堆内存数据
// users和data指向同一个堆内存,所以都变了
console.log(users[0].hobby); // ["读书", "跑步"]
console.log(data[0].hobby); // ["读书", "跑步"]
二、浅拷贝 vs 深拷贝
1. 浅拷贝(Shallow Copy)
- 本质:只拷贝栈内存中的引用地址,不拷贝堆内存中的实际数据
- 结果:多个变量指向同一个堆内存空间,修改一个会影响所有变量
- 常见场景:直接赋值(
let b = a)、Object.assign ()(单层)、数组 slice/concat(单层)
2. 深拷贝(Deep Copy)
- 本质:在堆内存中新建一块独立空间,把原数据完整复制进去,栈内存中存储新的地址
- 结果:多个变量指向不同的堆内存空间,修改一个不会影响其他变量
- 核心目标:切断引用关系,让拷贝后的变量完全独立
三、常用深拷贝方法
1. JSON 序列化 / 反序列化(最常用)
这是代码示例中用到的方法,原理是把对象转成 JSON 字符串(脱离引用关系),再转回新对象:
const users = [
{ id: 1, name: "张三", city: "北京" },
{ id: 2, name: "李四", city: "上海" }
];
// 深拷贝:JSON.parse + JSON.stringify
const data = JSON.parse(JSON.stringify(users));
// 修改data的堆内存数据,users不受影响
data[0].hobby = ["篮球", "旅行"];
console.log(data[0].hobby); // ["篮球", "旅行"]
console.log(users[0].hobby); // undefined(原数据无此属性)
注意事项:
- 优点:简单、无需依赖第三方库
- 缺点:无法拷贝函数、undefined、Symbol、循环引用对象、正则表达式等
2. 递归实现深拷贝(自定义)
如果需要处理 JSON 方法不支持的场景,可以手写递归函数:
function deepClone(obj) {
// 处理非对象类型(值类型)
if (obj === null || typeof obj !== 'object') return obj;
// 处理数组
if (obj instanceof Array) {
const newArr = [];
for (let i = 0; i < obj.length; i++) {
newArr[i] = deepClone(obj[i]); // 递归拷贝数组元素 } return newArr;
}
// 处理对象
if (obj instanceof Object) {
const newObj = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = deepClone(obj[key]); // 递归拷贝对象属性
}
}
return newObj;
}
}
// 使用示例
const users = [{ id: 1, info: { age: 20 } }];
const data = deepClone(users);
data[0].info.age = 30;
console.log(users[0].info.age); // 20(原数据不变)
四、实战避坑:什么时候需要深拷贝?
- 处理表单数据:编辑表单时,保留原始数据,避免修改编辑态影响原始数据
- 状态管理:Vue/React 中修改复杂状态对象,防止直接修改原状态导致的视图异常
- 数据缓存:拷贝接口返回的原始数据,后续加工不污染源数据
总结
- 浅拷贝是引用传递,多个变量共享同一块堆内存,修改会相互影响;深拷贝是值传递,新建独立堆内存,数据完全隔离。
- JSON 序列化(
JSON.parse(JSON.stringify()))是最便捷的深拷贝方式,但有数据类型限制;递归函数或 Lodash 的_.cloneDeep()能处理更复杂的场景。 - 核心逻辑:栈内存存简单值(值拷贝),堆内存存复杂数据(引用拷贝),深拷贝的本质是重新创建堆内存空间。
理解内存模型,就能彻底告别 "改 A 变 B" 的诡异 bug,写出更健壮的 JS 代码~