JavaScript基本数据类型与引用类型在内存存储上有什么区别
深入解析JavaScript基本数据类型与引用类型在栈内存与堆内存中的底层存储机制、值复制与引用传递原理,结合推导图解与实际编码陷阱进行全方位剖析。
在 JavaScript 中,基本数据类型与引用类型的核心内存差异在于:基本数据类型的值占用固定大小的空间,直接保存在调用栈(Stack)中;而引用数据类型的值大小不固定,保存在堆内存(Heap)中,栈内存中只存储指向堆中实际对象的内存地址(指针)。这种物理存储结构的不同,直接决定了变量赋值、函数参数传递、数据比较以及垃圾回收机制的本质行为。
一、JavaScript 运行时的双内存空间:栈与堆
JavaScript 引擎(如 V8)在执行代码时,会划分出不同的内存区域来管理数据,其中与数据存取最为密切的是调用栈(Call Stack)与内存堆(Memory Heap)。两者在结构和访问特性上有着明确的分工。
- 栈内存(Stack):由系统自动分配和释放,采用后进先出(LIFO)的数据结构。栈内存读写速度极快,空间相对较小且连续,专门用于存放函数调用的执行上下文、局部变量以及占用空间固定的小数据。
- 堆内存(Heap):由开发者根据业务逻辑动态分配,结构相对扁平且无序。堆内存空间庞大,适合存放结构复杂、大小可动态伸缩的对象,但通过内存地址寻址访问的速度略低于栈内存。
二、基本数据类型的存储:栈中的直接值存储
JavaScript 目前共有 7 种基本数据类型(又称原始数据类型):Number、String、Boolean、Undefined、Null、Symbol 和 BigInt。
基本数据类型的共同特点是结构简单、占用空间固定。当声明一个基本类型变量时,JavaScript 引擎会直接在当前的执行上下文栈中开辟对应大小的内存槽位,并将具体数值直接写入该槽位中。
1. 值复制机制与独立性
当把一个基本数据类型的变量赋值给另一个变量时,系统会在栈内存中重新开辟一段全新的空间,并将原变量的值完整复制一份存入新空间中。在此之后,两个变量各自独立,互不影响。
let x = 10;
let y = x; // 在栈中开辟新槽位存储 y,并将 x 的数值 10 复制给 y
y = 20; // 仅修改 y 对应槽位的值
console.log(x); // 输出 10,完全不受 y 变化的影响
console.log(y); // 输出 20
三、引用数据类型的存储:栈指针与堆对象的协同
引用数据类型通常指 Object,细分包括普通对象、数组(Array)、函数(Function)、日期(Date)、正则(RegExp)、Map、Set 等。
引用类型的数据结构无法预知具体大小,并且属性可以在运行时随时增删改查。如果强行存入栈中,频繁的动态伸缩会导致整个栈帧不断重新重排与内存重分配,极大地降低运行效率。因此:
- 堆内存负责存储对象本体:JavaScript 引擎在堆内存中按需开辟一块能够容纳属性键值对的空间,生成一个唯一的内存起始地址(如
0x0012FF)。 - 栈内存仅保存引用指针:在当前执行上下文的栈内存中,为该变量分配一个固定大小的槽位,槽位中保存的不是对象本身,而是上述堆内存的物理或逻辑地址。
1. 引用复制与属性共享的推导过程
当执行 let obj2 = obj1 时,发生的并非是堆中对象属性的深层复制,而是将栈中的地址指针复制了一份。此时 obj1 与 obj2 拥有不同的栈槽位,但槽位内部存储的地址相同,指向堆内存中同一块物理区域。
四、内存存储差异带来的核心开发影响
1. 全等比较(===)的判定逻辑
JavaScript 在执行 === 比较时,底层遵循统一的规则:直接比对栈内存中保存的内容是否完全一致。
- 对于基本类型:由于栈中保存的是“字面值”,因此只要数据类型一致且数值相等,
===即返回true。 - 对于引用类型:由于栈中保存的是“内存地址”,即使两个对象的内部键值对完全一模一样,只要它们分别在堆内存的不同地址初始化分配,栈中保存的指针就不相同,
{} === {}的判定结果必然是false。只有两个变量指向完全同一个堆地址时,全等比较才为true。
let a = { id: 1 };
let b = { id: 1 };
console.log(a === b); // false: 分别开辟在堆内存的不同地址,栈中指针不同
let c = a;
console.log(a === c); // true: 复制了栈中的内存地址,指向同一堆对象
2. 函数参数传递:全部都是“按值传递”
很多初学者容易误以为“基本类型按值传递,引用类型按引用传递”。但从底层的内存模型来看,JavaScript 中所有函数的参数传递本质上全部都是“按值传递”(Call by Value)。
所谓按值传递,指的是形参会在函数的局部栈帧中开辟独立空间,并将实参在栈中存放的内容按位复制过去:
- 如果传递基本类型,复制的是实际的数字、字符串等字面值;
- 如果传递引用类型,复制的是栈中的指针地址。由于形参拿到了相同的指针,在函数内修改对象属性会直接作用于堆内存中的实体;但如果在函数内部让形参重新指向一个新对象(修改形参本身的栈值),外层实参不会受到任何影响。
function modify(val, obj) {
val = 99; // 仅修改局部形参 val 的栈值
obj.name = "Modified"; // 通过指针修改堆内存共享对象
obj = { name: "Rebound" };// 将形参 obj 的栈指针指向新对象,不影响外层实参
}
let num = 1;
let myObj = { name: "Original" };
modify(num, myObj);
console.log(num); // 1 (无影响)
console.log(myObj.name); // "Modified" (属性被修改)
3. 垃圾回收(Garbage Collection)与生命周期
两种类型在生命周期管理上差异显著:
- 栈内存回收:随着函数调用完成,该函数的执行上下文直接从调用栈顶部弹出(Pop),栈指针下移,局部变量占用的栈空间被直接、立即回收,开销极低。
- 堆内存回收:堆中创建的对象无法通过简单的指针回退来释放。现代 JavaScript 引擎主要通过标记-清除(Mark-and-Sweep)算法进行周期性清理。垃圾回收器从根对象(全局对象、当前调用栈中的活动指针等)开始遍历,凡是无法通过引用链路访问到的堆对象,都会被判定为无用垃圾并统一释放。如果长期保留了对不再使用的堆对象的引用(如全局变量未置空、闭包未合理释放、未清理的事件监听器),就会导致内存泄漏。
五、全维度对比总结与最佳实践
为了直观掌握两种数据类型在内存存储与运行时特性上的本质不同,下表及全景对比图梳理了核心差异:
- 隔离对象修改:在需要复制对象并进行修改且不希望污染原数据时,浅拷贝(如解构运算符
{ ...obj })仅能复制第一层属性;深层嵌套对象必须使用structuredClone(obj)或深拷贝工具函数,确保在堆内存中完整克隆出全新的对象树。 - 避免内存泄漏:对于占用体积较大的对象或数组,若不再使用且处于全局或长生命周期闭包内,应及时显式赋值为
null,断开栈指针与堆对象的引用链路,帮助垃圾回收器尽快标记并回收堆内存。
JS大数值转字符串出现科学计数法怎么办
2026-11-05 17:09:21
JS split切割后出现空字符串怎么过滤
2026-11-05 16:00:30
在 React 或 Vue 项目中封装使用 JS 节流防抖
2026-11-05 15:27:29
JavaScript中怎么将带小数的字符串转为整数?
2026-11-05 14:32:38
怎么用JavaScript策略模式重构电商促销计算逻辑?
2026-11-05 14:01:09
相关标签:








