《JS 深拷贝:吃透栈堆,再也不踩引用坑》

15 阅读4分钟

从 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(原数据不变)

四、实战避坑:什么时候需要深拷贝?

  1. 处理表单数据:编辑表单时,保留原始数据,避免修改编辑态影响原始数据
  2. 状态管理:Vue/React 中修改复杂状态对象,防止直接修改原状态导致的视图异常
  3. 数据缓存:拷贝接口返回的原始数据,后续加工不污染源数据

总结

  1. 浅拷贝是引用传递,多个变量共享同一块堆内存,修改会相互影响;深拷贝是值传递,新建独立堆内存,数据完全隔离。
  2. JSON 序列化(JSON.parse(JSON.stringify()))是最便捷的深拷贝方式,但有数据类型限制;递归函数或 Lodash 的_.cloneDeep()能处理更复杂的场景。
  3. 核心逻辑:栈内存存简单值(值拷贝),堆内存存复杂数据(引用拷贝),深拷贝的本质是重新创建堆内存空间。

理解内存模型,就能彻底告别 "改 A 变 B" 的诡异 bug,写出更健壮的 JS 代码~