JS 一共有 8 种数据类型(ECMA262 规范)。搞懂"值存在哪里、怎么传",是理解所有 JS 行为的基础。
一、8 种数据类型总览
javascript
8 种数据类型
├── 原始数据类型(Primitive,共 7 种)
│ ├── Number 数值(整数、小数、Infinity、NaN)
│ ├── BigInt 大数(ES2020 新增,处理超大整数)
│ ├── Boolean 布尔(true / false)
│ ├── String 字符串
│ ├── Symbol 唯一标识符(ES6 新增)
│ ├── null 空的对象引用(有意为空)
│ └── undefined 未初始化 / 不存在
│
└── 引用数据类型(Reference,共 1 种,涵盖所有对象)
└── Object 对象(含 Array、Function、Date、RegExp 等)
规范另一种说法:原始数据类型 6 种 = null、undefined、numeric(再分 Number + BigInt)、Boolean、String、Symbol,加 1 种 Object,合计 8 种。
ES6 以前只有 6 种:Number、Boolean、String、null、undefined、Object。ES6 加 Symbol,ES2020 加大 BigInt,凑齐 8 种。
二、原始数据类型(简单类型)
1. Number(数值)
JS 所有数值统一用二进制浮点存,所以有个经典坑:
来自 3.js:
js
let a = 0.1;
let b = 0.2;
console.log(a + b); // 0.30000000000000004 不是 0.3!
原因:十进制小数转二进制会无限循环,只能截断保存,产生精度误差。比如 1/3 在十进制里是 0.333...,JS 用 64 位存,会有微小误差。
Number 包含的特殊值:
Infinity:正无穷大-Infinity:负无穷大NaN:Not a Number(比如'a' - 1得到 NaN)
2. BigInt(大数,ES2020 新增)
当 Number 存不下超大整数(超过 2^53-1 就会精度丢失),数据末尾加上n 就变成 BigInt ,可以精确存任意大的整数。
用 BigInt:
js
let num1 = 999999999999999999999999999999999999999999999999999999999999999n
let num2 = 123456789098765433467324577654789008733233456899003466788924243n
console.log(num1 + num2); // 能精确算出
console.log(typeof num1); // 'bigint'
console.log(num1 + 1n); // 加的数也要带 n 后缀
注意 :BigInt 字面量结尾要加 n,BigInt 不能和 Number 直接运算(类型不同)。
3. Boolean(布尔值)
只有两个值:true 和 false,用于条件判断。
4. String(字符串)
JS 字符串是不可变的,拼接/修改会创建新字符串,不会改原字符串。
5. Symbol(符号,ES6 新增)
唯一的标识符,用函数创建,本身是简单数据类型:
js
console.log(Symbol('张三') === Symbol('张三')); // false ← 即使描述一样,也是不同的
console.log(typeof Symbol('张三')); // 'symbol'
console.log(Symbol()); // 不传描述也行,绝对唯一
用途:作为对象的独一无二的属性 key,避免和其他属性名冲突:
js
let obj = {
[Symbol()]: 'value', // Symbol 作属性名,不会和普通属性重名
prop: '2'
};
口诀:Symbol = "传什么描述都是假的,创建出来就是独一份"。
6. null vs undefined(最易混的一对)
两者都表示"空/没有",但语义完全不同。
| null | undefined | |
|---|---|---|
| 语义 | 有意设置的空引用(应该有值,但现在没有) | 不应该有值的空(未初始化/不存在) |
| 出现场景 | ① 手动清空对象 obj = null ② 表示空的地址/联系/关系 |
① 声明变量未赋值(var/let) ② 对象属性不存在 ③ 函数没有 return ④ 访问不存在的数组索引 |
| 来自 1.js | obj.address = null(地址字段有意设为空) |
obj.age(age 属性不存在) |
| typeof | typeof null → 'object'(历史 Bug) |
typeof undefined → 'undefined' |
实战代码对比 (1.js):
js
let obj = {
name: "Alice",
address: null // ← 有意设为空:应该有地址这个字段,只是现在没填
};
console.log(obj.address); // null ← 字段存在,值是空的
console.log(obj.age); // undefined ← 连 age 这个字段都不存在
undefined 的四种场景:
js
// ① 声明变量但未赋值
let a;
console.log(a); // undefined
// ② 对象属性不存在
let obj = {};
console.log(obj.property); // undefined
// ③ 函数没有返回值
function noReturn() { }
console.log(noReturn()); // undefined
// ④ 访问不存在的数组索引
let arr = [1, 2, 3];
console.log(arr[5]); // undefined
null 的实际用途:清空对象释放内存 (1.js):
js
let largeObject = {
data: new Array(10000000).fill("hgh")
};
// 手动回收内存(告诉 GC 这个对象可以被回收)
largeObject = null;
typeof null === 'object' 的 Bug:
这是 JS 最著名的历史 bug。null 明明是原始类型,但 typeof null 返回 'object'。原因是早期 JS 用低 3 位存类型标记,对象标记是 000,null 表示全 0,所以被误判成对象。因为修复会导致太多老代码崩溃,至今保留。
一句话区分:
null = "特意留空" (应该有,但是现在没有);undefined = "根本没有"(没初始化/不存在)。
三、引用数据类型(复杂类型)
1. 所有对象都是 Object 系
javascript
Object
├── 普通对象 {name: 'xhj'}
├── 数组 [1, 2, 3]
├── 函数 function() {}
├── 日期 new Date()
├── 正则 /abc/
├── Map / Set
└── ... (所有你能用 new 造出来的)
虽然它们长得不一样,但在内存存储机制上都属于引用类型,行为一致。
2. 原始 vs 引用:赋值的区别(核心!)
js
// 原始数据类型:拷贝式赋值(复印机)
let a = null;
let b = a; // 复制值,a 和 b 互不影响
b = 2; // 改 b
console.log(a); // null ← a 不变
// 引用数据类型:引用式赋值(钥匙)
let obj1 = { name: "xhj" };
let obj2 = obj1; // 复制地址,obj1 和 obj2 指向同一个对象
obj2.company = "字节跳动"; // 通过 obj2 改
console.log(obj1, obj2); // 两者都有 company 属性!指向同一个对象
类比:
- 原始类型赋值 = 把文件复印一份,你改复印件,原件不变
- 引用类型赋值 = 给同一套房再配一把钥匙,用新钥匙进屋装修,用老钥匙打开一看也变了
四、内存分配:栈 vs 堆
1. 从冯诺依曼体系说起
现代计算机都遵循冯诺依曼架构:
┌──────────────────────────────────────────────┐
│ 运算器 + 控制器 = CPU(计算核心) │
│ 存储器:内存(快、小) + 外存/硬盘(慢、大) │
│ 输入设备:键盘、鼠标... │
│ 输出设备:屏幕、打印机... │
└──────────────────────────────────────────────┘
代码执行流程:
- 代码文件存在硬盘(外存)
- 编译时调入内存
- JS 在内存里进一步划分为栈 和堆
2. 栈内存(Stack)
特点:
- 快(直接 CPU 寻址,比堆快得多)
- 空间小(几 MB 级别)
- 先进后出管理
存什么:
- 原始数据类型 :直接存值本身 → 赋值时直接拷贝值(复印机)
- 引用数据类型的引用 :存堆内存的地址 → 赋值时拷贝地址(钥匙),对象本身还在堆里
- 函数执行上下文(变量环境 + 词法环境)
为什么栈这么快:
函数执行上下文对象占用多少空间在编译阶段就能算出来 ,推入调用栈时一次性分配合适的空间。当函数执行完出栈时,只需要移动一下栈顶指针(偏移量),就能立刻切换上下文。不需要 GC、不需要复杂的内存管理。
3. 堆内存(Heap)
特点:
- 相对慢一点(动态分配)
- 空间大(几百 MB 到几 GB,按需申请)
- 由 GC(垃圾回收器)管理回收
存什么:
- 引用数据类型本体(对象、数组、函数...的所有属性值)
- 大小不确定的、动态变化的数据
4. 画图理解
js
let a = 123; // 原始类型
let obj = { name: 'xhj' }; // 引用类型
css
栈内存(Stack) 堆内存(Heap)
┌─────────────────────┐ ┌──────────────────────────┐
│ 变量环境 / 词法环境 │ │ │
│ │ │ │
│ a: 123 │ ← 值直接存 │ 0x888: │
│ obj: 0x888 ──────────────────────--------->{ name: 'xhj' } │
│ ↑ │ (真实对象放在这里) │
│ 存的是地址(引用) | │ │
└─────────────────────┘ └──────────────────────────┘
5. 拷贝赋值 vs 引用赋值的内存视角
js
let obj1 = { name: "xhj" };
let obj2 = obj1; // 引用式赋值:栈里复制一份地址,指向堆里同一个对象
obj2.company = "字节跳动";
yaml
栈内存 堆内存
┌───────────────┐ ┌────────────────────────────────┐
│ obj1: 0xABC ───┼────────────────────▶│ 0xABC: { name: 'xhj', │
│ obj2: 0xABC ───┼────────────────────▶│ company: '字节跳动' } │
└───────────────┘ └────────────────────────────────┘
两把钥匙 同一套房子
这就是为什么改 obj2 会影响 obj1------两把钥匙开同一套房子。
6. 清空引用的正确方式
js
let largeObject = { data: bigArray };
largeObject = null;
内存视角 :把栈里的地址改成 null(断开引用),堆里的对象不再被任何栈引用,GC 就会在合适时机把它回收掉,释放内存。
五、完整类型检测
| 类型 | 写法 | 结果 | 注意 |
|---|---|---|---|
| Number | typeof 123 |
'number' |
--- |
| BigInt | typeof 123n |
'bigint' |
带 n |
| Boolean | typeof true |
'boolean' |
--- |
| String | typeof 'hi' |
'string' |
--- |
| Symbol | typeof Symbol() |
'symbol' |
--- |
| undefined | typeof undefined |
'undefined' |
--- |
| null | typeof null |
'object' |
❌ 历史 Bug,不能用 typeof 判 null |
| Object | typeof {a:1} |
'object' |
--- |
| Array | typeof [1,2,3] |
'object' |
❌ 要判数组用 Array.isArray(arr) |
| Function | typeof function(){} |
'function' |
✅ 唯一能区分函数的 typeof |
判 null 正确方法:
js
x === null // ✅ 直接三等比较,typeof null 不可靠
六、总结
1. 8 种数据类型速记
javascript
原始 7 种(栈里存值):
Number BigInt Boolean String Symbol null undefined
引用 1 种(栈里存地址,堆里存本体):
Object(数组、函数、Date、正则等都是 Object 系)
合计 8 种
2. null vs undefined
null = 特意留空 (应该有值,现在没填);undefined = 根本没有(未初始化/不存在)。
3. 栈 vs 堆
| 栈内存 | 堆内存 | |
|---|---|---|
| 速度 | 快 | 较慢 |
| 空间 | 小(几 MB) | 大(几百 MB+) |
| 存什么 | 原始值 + 引用地址 + 执行上下文 | 对象本体 |
| 管理方式 | 出栈即释放(指针偏移) | GC 垃圾回收 |
| 赋值行为 | 拷贝式(复印机) | 引用式(配钥匙) |
4. 核心心法
- 8 种类型,7 原始 + 1 引用
- typeof null === 'object' 是历史 Bug ,判 null 直接
=== null - 原始赋值 = 复印机 ,互不影响;引用赋值 = 配钥匙,一个改全改
- 栈存值/地址,堆存对象本体
var a; / obj.不存在 / 函数无 return / 数组越界→ undefined- 设空字段 = null,手动释放内存 = 置 null
一句话 :原始类型栈里存值,引用类型栈里存地址、堆里放对象,这就是所有差异的根源。