深入 V8 引擎:JavaScript 简单类型与引用类型的存储原理

47 阅读3分钟

JavaScript 数据类型避坑指南:赋值背后的栈内存与堆内存 🚀

前几天刚学完JS的执行机制,今天总结还不算晚。

毕竟step by step嘛


数据类型

在 JavaScript里,数据类型被 V8 引擎严格分为了两个大类,它们的待遇截然不同:

  1. 简单数据类型 (Primitives)

    • 栈:String, Number, Boolean, Undefined, Null, Symbol, BigInt。
  2. 引用数据类型 (Reference Types)

    • 堆:Object, Array, Function, Date 等。

栈内存 (Stack) :值的复印

我们先来看一段 简单数据类型 的代码

JavaScript

// 📄 简单数据类型的赋值
let str = 'hello'; 
let str2 = str;    // 值的拷贝(复印)

str2 = '你好';     // 修改副本

console.log(str, str2); 
// 输出:'hello', '你好'
// 互不影响

发生了什么?

简单数据类型存储在 栈内存 (Call Stack)  中。
栈内存是 V8 引擎为了维持程序执行上下文(Execution Context)而设立的,它的特点是存取速度极快,但空间有限。

当你执行 let str2 = str 时,V8 在栈内存中开辟了一个新的小隔间给 str2,并将 str 的值('hello')完完整整地复印了一份放进去。

  • str 就像一份原件文件。
  • str2 是你复印出来的副本。
  • 你在副本 str2 上涂改(变成'你好'),当然不会影响原件 str。

堆内存 (Heap) :共享的钥匙

接下来,高能预警!看看 引用数据类型 是如何“坑”到大家的

JavaScript

// 复杂数据类型的赋值
let obj = { 
    name: '张三',
    age: 18
};

// 引用式拷贝
let obj2 = obj; 

// 仅仅修改了 obj2
obj2.age++; 

console.log(obj2, obj);
// 输出:
// obj2: { name: '张三', age: 19 }
// obj:  { name: '张三', age: 19 } 
// 为什么 obj 也变了?!

对象(Object)的数据结构非常复杂,大小也不固定(你随时可能给对象添加新属性)。如果把它们也塞进栈内存,不仅会把栈撑爆,还会严重拖慢 CPU 的上下文切换速度。

所以,V8 采取了  “分离存储”  的策略:

  1. 数据实体:存储在空间巨大的 堆内存 (Heap)  中。
  2. 变量引用:在 栈内存 中,只保存该对象在堆内存中的 16进制内存地址(例如 0x0011A)。

钥匙的比喻

  • obj 在栈里存的不是房子(数据),而是一把房间钥匙(地址)。
  • 当你执行 let obj2 = obj 时,V8 并没有复制房子,而是配了一把一模一样的钥匙给 obj2。
  • 现在,obj 和 obj2 手里拿的钥匙,都打开的是堆内存里的同一个房间
  • 你通过 obj2 进屋把 age 打扫成了 19,当你通过 obj 进屋时,看到的自然也是 19。

V8 视角:为什么这么设计?

结合我们对 V8 执行机制的理解(基于 readme.md),这种设计是为了性能和效率的极致平衡:

  1. 栈 (Call Stack) 追求快

    • 栈负责管理函数的调用顺序(调用栈)。
    • 当一个函数执行完出栈时,栈内的简单变量会被迅速回收(LIFO 原则)。如果栈里塞满了巨大的对象,垃圾回收(GC)就会变得异常笨重,导致页面卡顿。
  2. 堆 (Heap) 追求大

    • 堆是动态分配的内存区域,适合存放那些生命周期长、体积大的数据(对象、闭包)。
    • 虽然堆的访问速度比栈慢,但它保证了栈的轻量级。

总结

  • 简单类型:在栈内存中,值拷贝。 你变我不变。
  • 引用类型:实体在堆,栈存地址,引用拷贝。 你变我也变。

避坑建议:
在开发中,如果你需要复制一个对象,且不希望修改新对象时影响原对象,千万不要直接用 = 赋值!
你需要进行 浅拷贝(如 Object.assign 或 ... 展开运算符)或 深拷贝(如 JSON.parse(JSON.stringify(obj)) 或 lodash.cloneDeep)。

作者:NEXT06