人人都会AI编程

7.5 常见数据类型的底层存储方式

更新时间:2026-07-11

理解数据在内存中如何存储,是深入掌握 JavaScript 行为的关键。为什么修改一个对象会影响到另一个变量?为什么基本类型的比较是值的比较,而对象的比较却是引用的比较?这一切的答案都藏在数据的存储方式里。

栈内存与堆内存的分工

JavaScript 引擎在运行时会使用两种主要的内存区域:

  • 栈内存(Stack):用于存储基本类型的值以及引用类型变量的指针。栈的特点是空间小、读写快、按顺序分配和释放,由系统自动管理。
  • 堆内存(Heap):用于存储引用类型的实际数据(对象、数组、函数等)。堆的特点是空间大、动态分配、读写相对较慢,通过垃圾回收释放。

两者分工的核心逻辑是:基本类型大小固定且使用频繁,栈内存能提供极快的存取速度;而引用类型大小不固定(可能动态增长),需要灵活的内存块,适合放在堆中。

基本类型的存储:值本身存在于栈中

对于 UndefinedNullBooleanNumberBigIntStringSymbol 这七种基本类型,引擎会直接在栈内存中创建空间来保存它们的

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 中被定义为基本类型,这似乎与直觉有些冲突:字符串可以很长,为什么也存储在栈中?实际上,现代引擎对字符串的处理很复杂,可能会使用一种“字符串常量池”的技术,或者在某些情况下将大字符串存储在堆中,但对外表现为不可变的“值”。更准确的描述是:字符串的行为完全符合基本类型的定义(不可变、按值比较),但引擎实现上可能为了效率将其主体放在堆上。 从开发者角度,只需牢记字符串作为基本类型,比较、赋值时行为与数字、布尔值一致即可。

内存管理的影响

理解了存储方式,就能避免大量常见的坑:

  • 避免意外共享修改:当需要拷贝对象来独立修改时,要使用深拷贝或浅拷贝,而不是直接赋值。
  • 防止内存泄漏:引用类型被多个变量引用时,即使删除了原始变量,只要还有引用指向它,垃圾回收就不会释放该内存。解除不用的引用有助于内存回收。
  • 理解相等比较:基本类型用 === 比较值,引用类型用 === 比较地址(要比较内容需要深度遍历或序列化)。

这些底层的存储机制,也直接为后续学习垃圾回收、性能优化、数据冻结等高级话题打下了坚实的基础。