JavaScript基本数据类型与引用类型在内存存储上有什么区别

作者:函数茶馆时间:2026-10-30 18:56:28浏览:116

深入解析JavaScript基本数据类型与引用类型在栈内存与堆内存中的底层存储机制、值复制与引用传递原理,结合推导图解与实际编码陷阱进行全方位剖析。

在 JavaScript 中,基本数据类型与引用类型的核心内存差异在于:基本数据类型的值占用固定大小的空间,直接保存在调用栈(Stack)中;而引用数据类型的值大小不固定,保存在堆内存(Heap)中,栈内存中只存储指向堆中实际对象的内存地址(指针)。这种物理存储结构的不同,直接决定了变量赋值、函数参数传递、数据比较以及垃圾回收机制的本质行为。

一、JavaScript 运行时的双内存空间:栈与堆

JavaScript 引擎(如 V8)在执行代码时,会划分出不同的内存区域来管理数据,其中与数据存取最为密切的是调用栈(Call Stack)与内存堆(Memory Heap)。两者在结构和访问特性上有着明确的分工。

  • 栈内存(Stack):由系统自动分配和释放,采用后进先出(LIFO)的数据结构。栈内存读写速度极快,空间相对较小且连续,专门用于存放函数调用的执行上下文、局部变量以及占用空间固定的小数据。
  • 堆内存(Heap):由开发者根据业务逻辑动态分配,结构相对扁平且无序。堆内存空间庞大,适合存放结构复杂、大小可动态伸缩的对象,但通过内存地址寻址访问的速度略低于栈内存。
JavaScript基本数据类型与引用类型在内存存储上有什么区别
图 1:JavaScript 栈内存(直接存放值与指针)与堆内存(存放实际对象)全景结构

二、基本数据类型的存储:栈中的直接值存储

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
JavaScript基本数据类型与引用类型在内存存储上有什么区别
图 2:基本数据类型在栈中分配独立槽位并直接保存数值,互不影响

三、引用数据类型的存储:栈指针与堆对象的协同

引用数据类型通常指 Object,细分包括普通对象、数组(Array)、函数(Function)、日期(Date)、正则(RegExp)、Map、Set 等。

引用类型的数据结构无法预知具体大小,并且属性可以在运行时随时增删改查。如果强行存入栈中,频繁的动态伸缩会导致整个栈帧不断重新重排与内存重分配,极大地降低运行效率。因此:

  1. 堆内存负责存储对象本体:JavaScript 引擎在堆内存中按需开辟一块能够容纳属性键值对的空间,生成一个唯一的内存起始地址(如 0x0012FF)。
  2. 栈内存仅保存引用指针:在当前执行上下文的栈内存中,为该变量分配一个固定大小的槽位,槽位中保存的不是对象本身,而是上述堆内存的物理或逻辑地址。

1. 引用复制与属性共享的推导过程

当执行 let obj2 = obj1 时,发生的并非是堆中对象属性的深层复制,而是将栈中的地址指针复制了一份。此时 obj1 与 obj2 拥有不同的栈槽位,但槽位内部存储的地址相同,指向堆内存中同一块物理区域。

JavaScript基本数据类型与引用类型在内存存储上有什么区别
图 3:引用类型操作推导(指针共享导致联动修改,而重定向赋值将切断共享链条)

四、内存存储差异带来的核心开发影响

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)算法进行周期性清理。垃圾回收器从根对象(全局对象、当前调用栈中的活动指针等)开始遍历,凡是无法通过引用链路访问到的堆对象,都会被判定为无用垃圾并统一释放。如果长期保留了对不再使用的堆对象的引用(如全局变量未置空、闭包未合理释放、未清理的事件监听器),就会导致内存泄漏。

五、全维度对比总结与最佳实践

为了直观掌握两种数据类型在内存存储与运行时特性上的本质不同,下表及全景对比图梳理了核心差异:

JavaScript基本数据类型与引用类型在内存存储上有什么区别
图 4:基本类型与引用类型在内存位置、访问特性及生命周期上的全维度对照
日常开发实用建议:
  1. 隔离对象修改:在需要复制对象并进行修改且不希望污染原数据时,浅拷贝(如解构运算符 { ...obj })仅能复制第一层属性;深层嵌套对象必须使用 structuredClone(obj) 或深拷贝工具函数,确保在堆内存中完整克隆出全新的对象树。
  2. 避免内存泄漏:对于占用体积较大的对象或数组,若不再使用且处于全局或长生命周期闭包内,应及时显式赋值为 null,断开栈指针与堆对象的引用链路,帮助垃圾回收器尽快标记并回收堆内存。
作者最新文章
相关文章
更多