JS 到底有多少种数据类型?从ECMA规范到内存本质,一文彻底搞懂

0 阅读7分钟

别再死记硬背了——这篇文章带你从内存分配的底层视角,重新理解 JavaScript 的 8 种数据类型。

开篇:一个经典面试题的"标准答案"

"JavaScript 有多少种数据类型?"

如果你刷过面试题,大概率能脱口而出:8 种。但当我追问你"为什么是 8 种?""null 和 undefined 到底有什么区别?""Symbol 和 BigInt 解决了什么痛点?""原始类型和引用类型在内存里到底长什么样?"——你能从底层原理讲清楚吗?

这篇文章不只是告诉你"是什么",更会带你从 ECMA262 规范、冯·诺依曼体系结构、调用栈与堆内存 的视角,把 JS 数据类型这件事彻底吃透。

一、ECMA262 规范怎么说?

根据 ECMA262 规范,JavaScript 的数据类型分为两大类:

类别类型说明
原始类型(Primitive)Undefined, Null, Boolean, String, Symbol, Number, BigInt7
对象类型(Object)Object1 种

其中 NumberBigInt 在规范中统一归类为 Numeric(数值类型),所以有时也会看到"6 种原始类型"的说法——实际上是把 Numeric 视为一个类别,其下再分为 Number 和 BigInt。但面试和日常讨论中,通常说 7 种原始类型 + 1 种对象类型 = 8 种

在 ES6 之前,JavaScript 只有 6 种类型:Undefined、Null、Boolean、String、Number、Object。ES6 新增了 Symbol,ES2020 又新增了 BigInt,这才凑齐了今天的 8 种。

二、最常见的两个"空值":null vs undefined

这是初学者最容易混淆的一组类型,但它们的设计意图完全不同。

null —— "故意为空"

null 表示一个被有意设置为空的对象引用。它是"此处应该有一个对象,但目前没有"的语义:

let obj = {
  name: "Alice",
  address: null  // 地址暂时不知道,但我知道这个字段应该存在
}

console.log(obj.address) // null  —— "我明确告诉你,这里是空的"
console.log(obj.age)     // undefined —— "压根没这个字段"

null 还有一个实用的用途——手动释放内存

let largeObject = {
  data: new Array(1000000).fill("hgh")
}
// 不再需要这个大对象时,赋值为 null
largeObject = null  // 切断引用链,让垃圾回收器回收这块内存

undefined —— "压根不存在"

undefined 表示变量未被初始化,或者你要找的东西根本不存在。它出现的典型场景有四个:

// 场景一:声明变量但未赋值
let a
console.log(a) // undefined

// 场景二:访问对象不存在的属性
let obj = {}
console.log(obj.prototype) // undefined

// 场景三:函数没有返回值
function noReturn() {
  // 没有 return
}
console.log(noReturn()) // undefined

// 场景四:访问不存在的数组索引
let arr = [1, 2, 3]
console.log(arr[3]) // undefined

一句话总结:null 是人类主动设置的空,undefined 是 JS 引擎告诉你的"这东西不存在"

三、Symbol —— 独一无二的标识符

Symbol 是 ES6 新增的原始类型,它的核心能力就两个字:唯一

console.log(Symbol('张老板') === Symbol('张老板')) // false
console.log(typeof Symbol('张老板'))               // 'symbol'

哪怕你传了相同的描述字符串(也叫 tag/label),每次调用 Symbol() 创建的都是一个全新的、独一无二的值。

Symbol 最常见的用途是防止对象属性名冲突

let obj = {
  [Symbol()]: 'value',  // 用 Symbol 做属性名
  prop: "2"
}
// 用 Symbol 作为 key 的属性,不会被常规遍历访问到

这在大型项目中非常有用——当你给一个第三方对象添加属性时,用 Symbol 做 key 可以保证绝对不会和已有属性冲突。

四、BigInt —— JS 终于能算大数了

一个经典的 JS 笑话:

console.log(0.1 + 0.2) // 0.30000000000000004

这不是 JS 的 bug,而是 IEEE 754 双精度浮点数标准的固有问题——二进制无法精确表示某些十进制小数,就像十进制无法精确表示 1/3 一样。

但更致命的问题是:JS 的 Number 类型能安全表示的最大整数只有 2^53 - 1

BigInt 就是来解决这个问题的:

let num1 = 999999999999999999999999999999999999999999999999999999999999999n
let num2 = 123456789098765433467324577654789008733233456899003466788924243n

console.log(num1 + num2)     // 正确的大整数相加结果
console.log(num1 + 1n)       // BigInt 只能和 BigInt 运算
console.log(typeof num1)     // 'bigint'

注意事项:BigInt 和 Number 不能混用num1 + 1 会直接报错,必须写成 num1 + 1n。这是 JS 在类型安全上向前迈出的一小步。

五、原始类型 vs 引用类型——内存分配的本质

这部分的面试题层出不穷,但如果你从内存分配的角度去理解,它就永远不会忘。

冯·诺依曼架构下的 JS 运行时

现代计算设备的根基是冯·诺依曼架构(运算器、控制器、存储器、输入设备、输出设备)。JS 代码的执行过程是:

  1. 代码存储在硬盘(外存)
  2. 执行时,代码从硬盘调入内存(RAM)
  3. JS 引擎创建执行上下文(变量环境 + 词法环境),逐行执行代码
  4. 函数调用时,执行上下文被推入调用栈

栈内存 vs 堆内存

调用栈(Stack)                  堆内存(Heap)
┌─────────────────┐              ┌──────────────────┐
│ 原始类型变量     │              │  对象(Object)   │
│ ─────────────── │              │  { name: "谢总",  │
│ a = null        │              │    company: "快手" │
│ b = 2           │              │  }                │
│                 │              │                   │
│ obj1 ───────────│──── 引用 ───▶│   地址 0x0012FF   │
│ obj2 ───────────│──── 引用 ───▶│   地址 0x0012FF   │
└─────────────────┘              └──────────────────┘
  • 原始类型:值直接写入栈内存。赋值时是拷贝式赋值——复印机一样,把值复制一份给新变量。
  • 引用类型(Object):对象本体存在堆内存中,栈内存里只存一个指向堆内存地址的指针。赋值时复制的是指针,所以多个变量会指向同一个对象。

代码验证:

// 原始类型:拷贝式赋值
let a = null
let b = a    // b 拿到了 a 的值(null),是两个独立变量
b = 2        // 改 b 不影响 a
console.log(a) // null

// 引用类型:引用式赋值
let obj1 = { name: "谢总" }
let obj2 = obj1       // obj2 拿到了 obj1 的引用地址,指向同一个对象
obj2.company = "快手"  // 改 obj2 指向的对象,obj1 也会看到变化
console.log(obj1, obj2) // { name: "谢总", company: "快手" } × 2

为什么这样设计?

栈内存的特点是快,但空间小。原始类型大小固定,编译阶段就能精确算出需要多少空间——函数执行完出栈时,只需要移动栈顶指针就可以释放整块空间,极快、极稳。

而对象是动态的,运行时才确定大小,可能随时增删属性——这种"不确定的、可变的"数据,只能扔到堆里管理,栈里存一个固定大小的指针就好。

六、一张全景速查表

类型分类含义示例typeof 结果
Number原始数值(IEEE 754)42, 0.1'number'
BigInt原始任意精度整数999n'bigint'
String原始字符串'hello''string'
Boolean原始布尔值true, false'boolean'
Undefined原始未定义undefined'undefined'
Null原始空值null'object' (历史bug)
Symbol原始唯一标识Symbol()'symbol'
Object引用对象/数组/函数等{}, []'object' / 'function'

一个小彩蛋:typeof null === 'object' 是 JS 诞生之初就留下的历史遗留 bug,因为 null 在底层被标记为了对象类型的空指针。这个 bug 永远不会被修复,因为会破坏太多现有代码。

七、写在最后

回到最初的问题——"JS 有多少种数据类型?"

如果你的答案是"8 种",恭喜你,面试官会点点头。

但如果你的答案是:

"7 种原始类型加 1 种对象类型,共 8 种。其中原始类型不可变、存栈,赋值靠拷贝;对象存堆,赋值靠引用。ES6 加了 Symbol 解决属性冲突问题,ES2020 加了 BigInt 弥补 Number 的大数精度短板。null 和 undefined 虽然都表示'空',但 null 是人为设置的空对象引用,undefined 是引擎层面的'未初始化'。"

那你不是在背书——你是真的懂了。


本文代码示例基于 ECMAScript 2020+ 标准,运行环境推荐 Node.js 14+ 或现代浏览器。

参考规范:ECMA-262 — ECMAScript® 2025 Language Specification