数据类型
一、JavaScript 的数据类型分类
简单数据类型(Primitive Types): number、string、boolean、undefined、null、symbol、bigint
⚠️ 注意:
typeof null === 'object'是历史 bug,但null仍是原始类型。
复杂数据类型(Reference Types / Objects): 普通对象、数组、函数、内置对象(Date, RegExp, Map, Set 等)、自定义类实例等
二、底层核心区别
| 维度 | 简单数据类型 | 复杂数据类型 |
|---|---|---|
| 存储方式 | 直接存储值 | 存储指向堆内存的引用(指针) |
| 内存位置 | 栈(Stack)或特殊区域(如字符串池) | 堆(Heap) |
| 赋值行为 | 值拷贝(copy by value) | 引用拷贝(copy by reference) |
| 可变性 | 不可变(immutable) | 可变(mutable) |
| 比较方式 | 比较值本身 | 比较引用地址(是否同一对象) |
三、详细解释
1. 内存布局与存储位置
🔹 简单类型:值直接内联存储:
- 在 V8 中,大多数原始值(如
number,boolean,undefined,null)被编码为 Smis(Small integers) 或 堆对象标签指针,但对开发者透明。 - 它们通常存储在:
- 栈帧(Stack Frame) 中(作为局部变量);
- 或直接嵌入在对象的字段中(如
obj.x = 42,42可能直接存于对象内部); - 字符串 虽是原始类型,但因长度可变,实际存储在堆上,但通过字符串表(string table) 实现去重和快速比较。
✅ 例如:
let a = 10; // 10 直接存于栈(或寄存器)
let b = 'hello'; // "hello" 存于堆,但 a b 变量本身存的是值或指针
🔹 复杂类型:变量存引用,对象存堆:
- 变量本身(如
arr)只保存一个 指针(pointer),指向堆中实际的对象。 - 对象本体(包括属性、元素、原型等)分配在 堆内存(Heap) 上。
let obj = { x: 1 };
// obj 变量(在栈)存的是一个地址,如 0x1234
// { x: 1 } 对象本体存于堆内存地址 0x1234
2. 赋值与传递:值拷贝 vs 引用拷贝
🔸 原始类型:完全拷贝值
🔸 引用类型:拷贝指针,共享对象
3. 不可变性(Immutability)
- 原始类型值本身不可变:
- 对象是可变的:
💡 注意:
const只防止变量重新赋值,不阻止对象内容修改。
4. 比较行为
- 原始类型:按值比较
- 引用类型:按引用比较
5. 垃圾回收(GC)影响
-
原始类型:
- 栈上的原始值随函数返回自动销毁;
- 堆中的字符串由 GC 管理,但因字符串表存在,可能长期存活。
-
引用类型:
- 对象分配在堆上,由 垃圾回收器(如 V8 的 Orinoco) 管理;
- 只有当对象不可达(unreachable) 时才会被回收;
- 引用类型更容易造成内存泄漏(如意外保留全局引用)。
四、V8 引擎中的具体实现细节
🔸 数字(Number)
- 小整数(-2³¹ ~ 2³¹-1)以 Smi(Small Integer) 形式直接存储在指针低位,无需堆分配。
- 浮点数或大整数则包装为 HeapNumber 对象,存于堆。
🔸 字符串(String)
- 存于堆,但 V8 使用 字符串哈希表(string table) 实现 interning:
- 相同内容的字符串共享同一内存地址;
- 提升
===比较性能(只需比地址)。
🔸 对象(Object)
- 在 V8 中,对象由 Map(隐藏类) + 属性存储(properties backing store) 构成;
- 属性可能存储在:
- 对象内联字段(fast properties);
- 外部字典(dictionary mode,用于动态属性)。
五、案例
let a = 100;
let b = 1e20;
let c = { age: 'abc', k: 10 };
let d = 'xyz';
📌 假设这段代码在一个函数内部执行(如全局作用域或普通函数),我们将从 变量本身(标识符) 和 值(value) 两个层面分析。
1. let a = 100;
a(变量名):存在当前函数的 栈帧 中。- 值
100:- 在 V8 中,
100是一个 Smi(Small Integer)。 - Smi 范围:-2³¹ 到 2³¹−1(即约 -21 亿 到 +21 亿)。
- Smi 被直接编码在指针的低位,无需堆分配。
- 因此,
100的值直接内联存储在变量a的栈槽中(或 CPU 寄存器)。
- 在 V8 中,
✅ 结论:a 和它的值都在 栈上(或寄存器),无堆分配。
2. let b = 1e20;
- 1e20 即
100000000000000000000 b(变量名):存在 栈帧 中。- 值
1e20:- 超出 Smi 范围(2¹⁰⁰ ≈ 1e30,但 Smi 只到 ~2e9)。
- V8 会将其包装为一个 HeapNumber 对象,分配在 堆上。
- 变量
b存的是指向该 HeapNumber 的指针。
✅ 结论:
b(变量)在 栈;- 实际数值对象在 堆。
🔍 验证:在 V8 中,
typeof 1e20 === 'number',但底层是堆对象。
3. let c = { ... };
这是一个对象字面量,涉及多层存储:
-
(1) 变量
c -
存在于 栈帧 中,值是一个指针,指向堆中的对象。
-
(2) 对象
{}本体 -
分配在 堆 上。
-
包含:
- 一个 Map(隐藏类):描述对象结构(属性名、顺序等);
- 属性存储区:存放
age和k的值。
-
(3) 属性值分析:
-
k: 10:10是 Smi → 直接内联存储在对象内部(无需额外堆分配)。
-
age: 'abc':- 这是一个长字符串 → 分配在 堆 上;
- V8 会检查字符串表(string table),如果之前没有相同内容,则新建;否则复用。
✅ 结论:
c(变量)在 栈;- 对象
{}、字符串'abc'都在 堆; - 数字
10内联在对象内部(仍在堆,但不额外分配)。
4. let d = 'xyz';
d(变量):在 栈帧 中。- 字符串值
'xyz':- 字符串在 JS 中是原始类型,但因长度 > 0 且不可变,必须分配在堆上。
- V8 会将其放入 字符串常量池(string table),实现去重。
- 如果之前已存在相同字符串,则
d指向已有地址; - 否则新建并加入表。
- 如果之前已存在相同字符串,则
✅ 结论:
d在 栈;- 字符串内容在 堆(字符串表)。
💡 即使是短字符串(如
'hi'),V8 也通常分配在堆,但通过 intern 优化比较和内存。
5. 可视化内存布局(概念图)
[ Stack Frame ]
┌─────────────┐
│ a: 100 │ ← Smi,值直接内联(无堆)
├─────────────┤
│ b: ptr → │ ───────────────┐
├─────────────┤ ↓
│ c: ptr → │ ───────→ [ Heap: Object { Map, props } ]
├─────────────┤ │ ├─ k: 10 (Smi, inline)
│ d: ptr → │ ───────┐ │ └─ age: ptr → [ Heap: String "ajjsjs..." ]
└─────────────┘ ↓ │
[ Heap: HeapNumber(1e20) ] ← b 指向这里
↑
[ Heap: String "xyz" ] ← d 指向这里
6. 补充说明
Q:为什么字符串不在栈上?A:因为字符串长度可变,且需要支持 === 快速比较(通过地址比较),所以统一放在堆 + 字符串表管理。
Q:Smi 为什么能省堆分配?A:V8 利用指针的最低位(总是 0,因内存对齐)来标记 Smi。例如:
- 普通指针:
0x12345678(末位 0) - Smi
100:100 << 1 = 200 = 0xC8,存为0x000000C8,最低位为 0 表示是 Smi。
Q:这些分配会影响性能吗?A:
- Smi 和内联属性访问极快;
- 堆分配会触发 GC,但现代 V8 的分代 GC(Orinoco)对短期对象回收非常高效。
7. 结论
| 变量 | 值类型 | 变量位置 | 值位置 | 是否堆分配 |
|---|---|---|---|---|
a | Smi (100) | 栈 | 栈(内联) | ❌ |
b | 大数 (1e20) | 栈 | 堆(HeapNumber) | ✅ |
c | 对象 | 栈 | 堆(Object) | ✅ |
c.k | Smi (10) | — | 对象内部(堆) | ❌(内联) |
c.age | 长字符串 | — | 堆(字符串表) | ✅ |
d | 字符串 | 栈 | 堆(字符串表) | ✅ |
六、总结
| 特性 | 简单类型(原始) | 复杂类型(引用) |
|---|---|---|
| 存储内容 | 实际值 | 指向堆对象的指针 |
| 内存位置 | 栈(或内联) | 堆(对象本体) |
| 赋值 | 拷贝值 | 拷贝引用 |
| 可变性 | 不可变 | 可变 |
| 比较 | 值相等 | 引用相等 |
| GC 压力 | 低 | 高(需追踪引用) |
💡 理解这一区别,是掌握 JavaScript 内存管理、性能优化和避免 bug(如意外共享对象)的基础。
如果你对 V8 如何表示 Smi、HeapObject 或字符串表感兴趣,我可以进一步展开!