别再死记硬背了------这篇文章带你从内存分配的底层视角,重新理解 JavaScript 的 8 种数据类型。
开篇:一个经典面试题的"标准答案"
"JavaScript 有多少种数据类型?"
如果你刷过面试题,大概率能脱口而出:8 种。但当我追问你"为什么是 8 种?""null 和 undefined 到底有什么区别?""Symbol 和 BigInt 解决了什么痛点?""原始类型和引用类型在内存里到底长什么样?"------你能从底层原理讲清楚吗?
这篇文章不只是告诉你"是什么",更会带你从 ECMA262 规范、冯·诺依曼体系结构、调用栈与堆内存 的视角,把 JS 数据类型这件事彻底吃透。
一、ECMA262 规范怎么说?
根据 ECMA262 规范,JavaScript 的数据类型分为两大类:
| 类别 | 类型 | 说明 |
|---|---|---|
| 原始类型(Primitive) | Undefined, Null, Boolean, String, Symbol, Number, BigInt | 共 7 种 |
| 对象类型(Object) | Object | 1 种 |
其中 Number 和 BigInt 在规范中统一归类为 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 表示一个被有意设置为空的对象引用。它是"此处应该有一个对象,但目前没有"的语义:
javascript
let obj = {
name: "Alice",
address: null // 地址暂时不知道,但我知道这个字段应该存在
}
console.log(obj.address) // null ------ "我明确告诉你,这里是空的"
console.log(obj.age) // undefined ------ "压根没这个字段"
null 还有一个实用的用途------手动释放内存:
javascript
let largeObject = {
data: new Array(1000000).fill("hgh")
}
// 不再需要这个大对象时,赋值为 null
largeObject = null // 切断引用链,让垃圾回收器回收这块内存
undefined ------ "压根不存在"
undefined 表示变量未被初始化,或者你要找的东西根本不存在。它出现的典型场景有四个:
javascript
// 场景一:声明变量但未赋值
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 新增的原始类型,它的核心能力就两个字:唯一。
javascript
console.log(Symbol('张老板') === Symbol('张老板')) // false
console.log(typeof Symbol('张老板')) // 'symbol'
哪怕你传了相同的描述字符串(也叫 tag/label),每次调用 Symbol() 创建的都是一个全新的、独一无二的值。
Symbol 最常见的用途是防止对象属性名冲突:
javascript
let obj = {
[Symbol()]: 'value', // 用 Symbol 做属性名
prop: "2"
}
// 用 Symbol 作为 key 的属性,不会被常规遍历访问到
这在大型项目中非常有用------当你给一个第三方对象添加属性时,用 Symbol 做 key 可以保证绝对不会和已有属性冲突。
四、BigInt ------ JS 终于能算大数了
一个经典的 JS 笑话:
javascript
console.log(0.1 + 0.2) // 0.30000000000000004
这不是 JS 的 bug,而是 IEEE 754 双精度浮点数标准的固有问题------二进制无法精确表示某些十进制小数,就像十进制无法精确表示 1/3 一样。
但更致命的问题是:JS 的 Number 类型能安全表示的最大整数只有 2^53 - 1。
BigInt 就是来解决这个问题的:
javascript
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 代码的执行过程是:
- 代码存储在硬盘(外存)
- 执行时,代码从硬盘调入内存(RAM)
- JS 引擎创建执行上下文(变量环境 + 词法环境),逐行执行代码
- 函数调用时,执行上下文被推入调用栈
栈内存 vs 堆内存
ini
调用栈(Stack) 堆内存(Heap)
┌─────────────────┐ ┌──────────────────┐
│ 原始类型变量 │ │ 对象(Object) │
│ ─────────────── │ │ { name: "谢总", │
│ a = null │ │ company: "快手" │
│ b = 2 │ │ } │
│ │ │ │
│ obj1 ───────────│──── 引用 ───▶│ 地址 0x0012FF │
│ obj2 ───────────│──── 引用 ───▶│ 地址 0x0012FF │
└─────────────────┘ └──────────────────┘
- 原始类型 :值直接写入栈内存 。赋值时是拷贝式赋值------复印机一样,把值复制一份给新变量。
- 引用类型(Object) :对象本体存在堆内存 中,栈内存里只存一个指向堆内存地址的指针。赋值时复制的是指针,所以多个变量会指向同一个对象。
代码验证:
javascript
// 原始类型:拷贝式赋值
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