JavaScript基本数据类型与引用类型在内存存储上有什么区别
深入解析JavaScript基本数据类型与引用类型在栈内存与堆内存中的存储机制差异,结合变量复制、函数传参及修改影响,提供清晰的图文对照与实践指南。
在 JavaScript 中,数据类型被划分为两大类:基本数据类型(Primitive Types)和引用数据类型(Reference Types)。它们在内存存储上的核心区别可以概括为一句话:基本数据类型的值大小固定,直接按值保存在栈内存(Stack)中;引用数据类型的值大小动态且结构复杂,实体保存在堆内存(Heap)中,而栈内存中仅保存指向该堆内存地址的引用指针。
一、栈内存与堆内存的基本分工
为了理解这两种类型的存储差异,首先需要明白 JavaScript 运行环境中的两块主要内存区域的作用机制:
- 栈内存(Stack):由系统自动分配和释放,具有“后进先出(LIFO)”的高效线性数据结构。栈内存的访问速度极快,但空间相对有限,因此只适合存放生命周期明确、占用空间大小固定的数据,例如当前执行上下文中的局部变量标识符、基本类型的值以及对象的内存地址指针。
- 堆内存(Heap):是一块动态分配、结构不受限制的无序内存池。堆内存空间大,可以存储结构多变、属性可随时增删的对象实体。但由于需要通过指针进行寻址分配和垃圾回收清理,访问速度相比栈内存稍慢。
二、两类数据类型的具体分类与特征
JavaScript 语言标准目前定义了 8 种数据类型,其存储归属如下:
| 分类 | 包含类型 | 存储位置 | 数据大小 | 不可变性 |
|---|---|---|---|---|
| 基本数据类型 | Number, String, Boolean, Undefined, Null, Symbol, BigInt |
栈内存(Stack) | 固定大小 | 值本身不可变(只读替换) |
| 引用数据类型 | Object, Array, Function, Date, RegExp, Map, Set 等 |
堆内存(Heap),栈中存指针 | 大小可动态增减 | 内容可直接在原内存上修改 |
注意细节:基本类型的值具有“不可变性(Immutability)”。例如对一个字符串变量进行拼接或替换字符,并不是在原有内存位置修改字符,而是开辟新空间生成新字符串并将变量指向新值;而引用类型的对象属性增删、数组元素的修改,都是直接在堆内存的原实体上操作。
三、变量声明与内存分配推导步骤
当一段 JavaScript 脚本执行时,引擎是如何一步步在底层分配这些内存的?下面通过一个包含基本类型和引用类型的具体代码段进行推导。
// 执行代码示例
let count = 10;
let user = { id: 101, role: "admin" };
let list = [1, 2];
四、赋值与修改:值的复制 vs 引用的复制
内存存储位置的差异,直接决定了当把一个变量赋值给另一个变量时的行为模式。
1. 基本类型的按值复制
当把基本类型变量 a 赋值给 b 时,栈内存会为 b 开辟一块全新的独立空间,并把 a 的值完整复制过去。此后无论修改哪一个,彼此之间完全独立互不影响。
let a = 100;
let b = a; // 栈中新开辟空间,b = 100
b = 200;
console.log(a); // 100,原有变量不受任何影响
console.log(b); // 200
2. 引用类型的按引用复制
当把一个对象变量 obj1 赋值给 obj2 时,由于对象保存在堆中,栈中保存的仅仅是内存地址指针。因此,这次赋值只是在栈中复制了一份地址指针,两个变量最终指向堆内存中完全相同的同一块实体。
let obj1 = { score: 90 };
let obj2 = obj1; // 仅复制了指针,指向同一个堆对象
obj2.score = 100;
console.log(obj1.score); // 100!obj1 被间接修改
console.log(obj2.score); // 100
五、函数参数传递的本质验证
很多开发者常讨论“JavaScript 是按值传递还是按引用传递?”从底层内存机制来看,答案非常明确:JavaScript 中所有函数的参数传递都是按值传递(Pass by value)。
- 对于基本类型,传递的是实际数值的副本。在函数内部对形参重新赋值,绝不会影响外部原变量。
- 对于引用类型,传递的是保存在栈中的地址指针的副本。虽然复制的是指针,但由于副本和原指针存储的是同一个堆地址,修改形参属性会影响外部对象;但若在函数内给形参重新赋值一个新对象(即覆盖指针本身),外部原对象依然不受影响。
function modify(val, obj) {
val = 999; // 修改形参的基本值副本
obj.name = "Updated"; // 顺着指针修改堆中的对象属性
obj = { name: "Replaced" }; // 形参指针指向了新对象,切断了与原有对象的关联
}
let num = 10;
let person = { name: "Initial" };
modify(num, person);
console.log(num); // 10(完全不受影响)
console.log(person.name); // "Updated"(属性被修改)
六、日常开发中的注意事项与最佳实践
理解栈与堆的存储差异,能够直接指导我们在编写代码时避开常见隐患:
- 避免意外的对象副作用:如果需要一个对象的独立副本,切勿直接使用
let copy = original;。应当根据结构层级选择浅拷贝(如{ ...original }、Object.assign())或深拷贝(如structuredClone(original))。 - 理解判等符(== 与 ===):
- 两个基本类型比较时,比对的是栈内存中的值本身(如
10 === 10为 true)。 - 两个引用类型比较时,比对的是栈内存中的地址指针。哪怕两个对象内容完全一样,只要它们在堆中不是同一个对象,比较结果也是 false(如
{} === {}为 false)。
- 两个基本类型比较时,比对的是栈内存中的值本身(如
- 及时解除引用以协助垃圾回收:保存在堆中的对象如果没有任何栈变量持有其引用指针(引用计数为 0 或从根不可达),垃圾回收器(GC)就会将其回收。对于使用完毕的全局大对象,适时设置
obj = null能够切断引用链,释放堆内存。
three.js加载obj模型的实例代码
2026-10-22 09:49:21








