理解数据在内存中如何存储,是深入掌握 JavaScript 行为的关键。为什么修改一个对象会影响到另一个变量?为什么基本类型的比较是值的比较,而对象的比较却是引用的比较?这一切的答案都藏在数据的存储方式里。
栈内存与堆内存的分工
JavaScript 引擎在运行时会使用两种主要的内存区域:
- 栈内存(Stack):用于存储基本类型的值以及引用类型变量的指针。栈的特点是空间小、读写快、按顺序分配和释放,由系统自动管理。
- 堆内存(Heap):用于存储引用类型的实际数据(对象、数组、函数等)。堆的特点是空间大、动态分配、读写相对较慢,通过垃圾回收释放。
两者分工的核心逻辑是:基本类型大小固定且使用频繁,栈内存能提供极快的存取速度;而引用类型大小不固定(可能动态增长),需要灵活的内存块,适合放在堆中。
基本类型的存储:值本身存在于栈中
对于 Undefined、Null、Boolean、Number、BigInt、String 和 Symbol 这七种基本类型,引擎会直接在栈内存中创建空间来保存它们的值。
let a = 42;
let b = "hello";
let c = true;
上述代码执行后,栈内存中大致会存储这样的信息:
栈内存:
标识符 a → 值 42
标识符 b → 值 "hello"
标识符 c → 值 true
每个变量名直接关联到其具体值,没有任何中间引用。值的独立拷贝:当把一个基本类型变量赋值给另一个变量时,发生的是值的拷贝,两者各自拥有独立的内存空间,互不影响。
let x = 10;
let y = x; // y 拷贝了 x 的值 10
x = 20;
console.log(y); // 10,不受影响
引用类型的存储:栈中存地址,堆中存内容
所有 Object 类型的数据(普通对象、数组、函数、Map、Set 等)存储方式不同:栈内存中只存放一个指向堆内存实际数据的引用地址(指针),真正的对象内容存放在堆内存中。
let obj = { name: "Alice", age: 25 };
let arr = [1, 2, 3];
内存布局大致如下:
栈内存:
标识符 obj → 地址 0x001(指向堆内存的对象)
标识符 arr → 地址 0x002(指向堆内存的数组)
堆内存:
地址 0x001 → { name: "Alice", age: 25 }
地址 0x002 → [1, 2, 3]
引用的共享:当将一个引用类型变量赋值给另一个变量时,拷贝的是栈中的引用地址,而不是堆中的实际对象。因此,两个变量会指向堆内存中的同一份数据,通过任一变量修改对象,都会影响另一个。
let obj1 = { value: 1 };
let obj2 = obj1; // obj2 拷贝的是引用地址,指向同一对象
obj2.value = 2;
console.log(obj1.value); // 2,两者引用同一数据
这种机制也解释了为什么比较两个对象时,比较的是引用地址,而不是内容:
let a = {};
let b = {};
console.log(a === b); // false,不同的引用地址
函数参数的传递本质
JavaScript 中所有函数参数都是按值传递的(ECMAScript 规范明确规定了这一点)。但如果参数是引用类型,传递的值其实是引用地址的拷贝,这造成了类似“按引用传递”的效果:
- 传递基本类型:函数内修参数不会影响外部变量。
- 传递引用类型:函数内修改对象属性会影响外部对象,因为参数拷贝了引用地址,仍指向同一堆内存。但如果将参数重新赋值为一个新对象,外部变量保持不变,因为改变的只是拷贝过来的那个地址值。
function change(num, obj) {
num = 100; // 修改的是局部拷贝,不影响外部
obj.name = "Bob"; // 修改了堆中的对象,外部可见
}
let count = 5;
let person = { name: "Alice" };
change(count, person);
console.log(count); // 5,未变
console.log(person.name); // "Bob",被修改了
字符串的特殊性
字符串在 ECMAScript 中被定义为基本类型,这似乎与直觉有些冲突:字符串可以很长,为什么也存储在栈中?实际上,现代引擎对字符串的处理很复杂,可能会使用一种“字符串常量池”的技术,或者在某些情况下将大字符串存储在堆中,但对外表现为不可变的“值”。更准确的描述是:字符串的行为完全符合基本类型的定义(不可变、按值比较),但引擎实现上可能为了效率将其主体放在堆上。 从开发者角度,只需牢记字符串作为基本类型,比较、赋值时行为与数字、布尔值一致即可。
内存管理的影响
理解了存储方式,就能避免大量常见的坑:
- 避免意外共享修改:当需要拷贝对象来独立修改时,要使用深拷贝或浅拷贝,而不是直接赋值。
- 防止内存泄漏:引用类型被多个变量引用时,即使删除了原始变量,只要还有引用指向它,垃圾回收就不会释放该内存。解除不用的引用有助于内存回收。
- 理解相等比较:基本类型用
===比较值,引用类型用===比较地址(要比较内容需要深度遍历或序列化)。
这些底层的存储机制,也直接为后续学习垃圾回收、性能优化、数据冻结等高级话题打下了坚实的基础。