跳到主要内容

数据类型

一、JavaScript 的数据类型分类

简单数据类型(Primitive Types): numberstringbooleanundefinednullsymbolbigint

⚠️ 注意: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 = 4242 可能直接存于对象内部);
    • 字符串 虽是原始类型,但因长度可变,实际存储在堆上,但通过字符串表(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 寄存器)。

结论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(隐藏类):描述对象结构(属性名、顺序等);
    • 属性存储区:存放 agek 的值。
  • (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 100100 << 1 = 200 = 0xC8,存为 0x000000C8,最低位为 0 表示是 Smi。

Q:这些分配会影响性能吗?A:

  • Smi 和内联属性访问极快;
  • 堆分配会触发 GC,但现代 V8 的分代 GC(Orinoco)对短期对象回收非常高效。

7. 结论

变量值类型变量位置值位置是否堆分配
aSmi (100)栈(内联)
b大数 (1e20)堆(HeapNumber)
c对象堆(Object)
c.kSmi (10)对象内部(堆)❌(内联)
c.age长字符串堆(字符串表)
d字符串堆(字符串表)

六、总结

特性简单类型(原始)复杂类型(引用)
存储内容实际值指向堆对象的指针
内存位置栈(或内联)堆(对象本体)
赋值拷贝值拷贝引用
可变性不可变可变
比较值相等引用相等
GC 压力高(需追踪引用)

💡 理解这一区别,是掌握 JavaScript 内存管理、性能优化和避免 bug(如意外共享对象)的基础。

如果你对 V8 如何表示 Smi、HeapObject 或字符串表感兴趣,我可以进一步展开!