JavaScript 拷贝机制解析:从内存存储到代码中的值拷贝与深拷贝
在 JavaScript 中,变量的存储和拷贝方式是理解数据操作的核心知识点。不同类型的数据在内存中的存储位置不同,这直接导致了拷贝行为的差异。本文将结合具体代码,详细解析值拷贝、引用拷贝以及深拷贝的概念与实现。
内存存储的两种空间:栈内存与堆内存
JavaScript 引擎将内存分为栈内存和堆内存,分别用于存储不同类型的数据:
- 栈内存:用于存储简单数据类型(如数字、字符串、布尔值等),特点是空间连续、读写高效,变量的操作直接作用于值本身。
- 堆内存:用于存储复杂数据类型(如对象、数组等),特点是空间动态分配(弹性变化)、内存开销较大,变量存储的是指向堆内存中数据的引用地址(类似 “指针”)。
引用拷贝:共享同一份堆内存数据
先看clone/1.js中的代码示例,它展示了引用拷贝的特性:
// 复杂数据类型(数组)存储在堆内存中,users变量存储的是引用地址
const users = [
{
id: 1,
name: "张三",
hometown: "南昌"
},
{
id: 2,
name: "李四",
hometown: "南昌"
},
{
id: 3,
name: "王五",
hometown: "进贤"
}
];
// 数组是引用类型,push操作修改的是堆内存中的数据(引用地址不变)
// 因此const声明的users可以被修改内容(const仅限制引用地址不可变)
users.push({
id: 4,
name: "赵六",
hometown: "南昌"
});
// 简单数据类型(数字)存储在栈内存中,赋值操作是值拷贝(独立副本)
let a = 1;
let d = a; // d是a的副本,修改d不会影响a
// 引用拷贝:data与users存储相同的引用地址,指向同一份堆内存数据
const data = users;
// 修改data中的数据,会影响原users(因为共享同一份堆内存)
data[0].hobbies = ["篮球", "看烟花"];
// 打印结果:data和users的第0个元素都包含hobbies属性
console.log(data, users);
代码解析:当我们执行const data = users时,并非将users对应的数组数据复制一份,而是将users存储的引用地址赋值给了data。此时data和users指向堆内存中同一份数组数据,因此修改data中的元素会同步影响users,这就是引用拷贝的特性。
深拷贝:创建独立的堆内存数据
如果希望拷贝后的数据与原数据完全独立(修改拷贝后的数据不影响原数据),就需要使用深拷贝。clone/2.js展示了一种常见的深拷贝实现方式:
// 声明变量(初始为undefined,存储在栈内存)
var users;
var data;
// 数组存储在堆内存,users存储引用地址
users = [ { id: 1, name: "张三", hometown: "南昌" }, { id: 2, name: "李四", hometown: "南昌" }, { id: 3, name: "王五", hometown: "进贤" }];
// 深拷贝:通过JSON序列化与反序列化实现
// 1. JSON.stringify(users):将对象转为JSON字符串(脱离堆内存)
// 2. JSON.parse(...):将字符串转回对象,此时会在堆内存中创建新空间存储数据
var data = JSON.parse(JSON.stringify(users));
// 修改data中的数据,不会影响原users(两者指向不同堆内存)
data[0]['hobbies'] = ["篮球", "看烟花"];
// 打印结果:data的第0个元素有hobbies,users无此属性
console.log(data, users);
代码解析:JSON.parse(JSON.stringify(users))的本质是:先将堆内存中的对象转为字符串(脱离原内存),再将字符串重新解析为新对象。这个过程会在堆内存中申请一块新空间存储拷贝后的数据,data存储的是新空间的引用地址。因此data与users指向完全独立的两份数据,修改data不会影响users,这就是深拷贝的效果。
总结:值拷贝、引用拷贝与深拷贝的区别
| 拷贝类型 | 适用数据类型 | 拷贝原理 | 修改影响 |
|---|---|---|---|
| 值拷贝 | 简单类型(数字、字符串等) | 直接拷贝值到栈内存新空间 | 修改拷贝后的值不影响原数据 |
| 引用拷贝 | 复杂类型(对象、数组等) | 拷贝引用地址,共享堆内存数据 | 修改拷贝后的数据会影响原数据 |
| 深拷贝 | 复杂类型(对象、数组等) | 重新创建堆内存空间,拷贝完整数据 | 修改拷贝后的数据不影响原数据 |
在实际开发中,需根据需求选择合适的拷贝方式:若需共享数据,可使用引用拷贝;若需独立操作数据,则需使用深拷贝(除了 JSON 方法,还可通过递归遍历等方式实现)。