JavaScript 拷贝机制解析:从内存存储到代码中的值拷贝与深拷贝

49 阅读4分钟

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。此时datausers指向堆内存中同一份数组数据,因此修改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存储的是新空间的引用地址。因此datausers指向完全独立的两份数据,修改data不会影响users,这就是深拷贝的效果。

总结:值拷贝、引用拷贝与深拷贝的区别

拷贝类型适用数据类型拷贝原理修改影响
值拷贝简单类型(数字、字符串等)直接拷贝值到栈内存新空间修改拷贝后的值不影响原数据
引用拷贝复杂类型(对象、数组等)拷贝引用地址,共享堆内存数据修改拷贝后的数据会影响原数据
深拷贝复杂类型(对象、数组等)重新创建堆内存空间,拷贝完整数据修改拷贝后的数据不影响原数据

在实际开发中,需根据需求选择合适的拷贝方式:若需共享数据,可使用引用拷贝;若需独立操作数据,则需使用深拷贝(除了 JSON 方法,还可通过递归遍历等方式实现)。