js
let a = null;
let b = a;
b = 2;
console.log(a, b);
let obj1 = { name: '张三' };
let obj2 = obj1;
obj2.company = '快手';
console.log(obj1, obj2);
// 输出:
// null 2
// { name: '张三', company: '快手' } { name: '张三', company: '快手' }
同样是赋值,b = 2 没有影响 a,但 obj2.company = '快手' 却把 obj1 也改了。这不是 bug------这是 JS 数据类型在内存层面的根本差异。
立意句:理解 JS 八种数据类型的分类标准和内存存储差异,能用正确的类型判断和赋值方式避免 null/undefined 混淆、浮点数精度丢失、以及引用赋值带来的副作用。
1. 全景:JS 到底有多少种类型?
根据 ECMA262 规范,JS 共有 8 种 数据类型,分两大阵营。其中原始类型中有一个 numeric 子分类------把 Number 和 BigInt 归在一起,因为它们都是"数值"。
| 分类 | 子分类 | 类型 | 说明 |
|---|---|---|---|
| 原始类型 | --- | String | 字符串 |
| 原始类型 | --- | Boolean | 布尔值 |
| 原始类型 | --- | null | 有意设置为空的对象引用 |
| 原始类型 | --- | undefined | 未初始化或不存在的值 |
| 原始类型 | --- | Symbol(ES6 新增) | 唯一标识符 |
| 原始类型 | numeric | Number | 整数和浮点数统一(64位双精度) |
| 原始类型 | numeric | BigInt(ES6 新增) | 任意精度大整数 |
| 引用类型 | --- | Object | 对象、数组、函数等 |
原始类型共 6 种(ES6 前)→ ES6 新增 Symbol 和 BigInt 后变成 7 种原始 + 1 种引用 = 8 种。记住这个分类,是后续所有讨论的底盘。
2. 原始类型:写在栈上的"值本身"
原始类型的变量,存储的是值本身。给另一个变量赋值时,行为像复印机------复印一份新的,原件和复印件互不影响。
js
let a = null;
let b = a; // 拷贝值
b = 2;
console.log(a, b);
// 输出:null 2
改 b 不影响 a,因为 b 拿到的是一份独立拷贝。
2.1 null vs undefined:最容易混淆的两个原始类型
它俩长得像,但语义完全不同。
null ------ 空值。表示"这里应该有值,但目前没有",是你主动设的。
js
let obj = {
name: '张三',
address: null // 地址应该有值,目前没收集到
};
console.log(obj.age); // undefined --- 压根没定义过这个属性
console.log(obj.address); // null --- 定义了,但设成了空
null 还能用来手动释放内存:
js
let largeObject = {
data: new Array(100000000).fill('haha')
};
largeObject = null; // 切断引用,让垃圾回收器回收
undefined ------ 未定义。表示"这里根本没放过值",是系统的默认状态。四种典型场景:
js
let a; // 1. 声明变量但未赋值
console.log(a); // undefined
let obj = {};
console.log(obj.name); // 2. 访问对象不存在的属性 → undefined
function noReturn() {}
console.log(noReturn());// 3. 函数没有返回值 → undefined
let arr = [1, 2, 3];
console.log(arr[5]); // 4. 访问不存在的数组索引 → undefined
简单区分:你用等号写 null,JS 引擎自己给你 undefined。
3. ES6 新增的两个原始类型
3.1 BigInt:JS 终于能算大数了
JS 的数字用 64 位二进制存储,整数安全范围是 -(2^53 - 1) 到 2^53 - 1。超出这个范围,精度就丢了。更糟的是------小数也算不准。
js
let a = 0.1;
let b = 0.2;
console.log(a + b);
// 输出:0.30000000000000004
不是 JS 算错了,是二进制浮点数没法精确表示十进制小数------就像十进制没法精确表示 1/3(0.33333...)。有小数精度的场景(比如金额计算),要么用整数(单位换成分),要么用第三方库。
BigInt 解决的是大整数精度问题------数字末尾加 n 就是 BigInt 字面量:
js
let num1 = 999999999999999999999999999999999999999999999999999999999999999n;
let num2 = 123456789098765433467324577654789008733233456899003466788924243n;
console.log(num1 + num2, typeof num1);
// 输出:(正确的大数结果) bigint
注意:BigInt 只能和 BigInt 做运算,不能混用 Number。
1n + 1会直接报TypeError。
3.2 Symbol:独一无二的标识符
当你需要一个绝对不会和别人重复的 key,用 Symbol。
js
console.log(Symbol('张三') === Symbol('张三'));
// 输出:false --- 即使标签相同,每次调用 Symbol() 都返回一个全新的值
console.log(typeof Symbol('张三'));
// 输出:'symbol'
let obj = {
[Symbol()]: 'value',
prop: '2'
};
// Symbol 作为 key 的属性,普通遍历(for...in、Object.keys)拿不到它
Symbol() 就是一个唯一值生成器------你传的字符串只是标签 (方便调试),不是值本身。两次 Symbol('张三') 就像两个人同名同姓,但他们是不同的人。
4. 引用类型与内存:栈和堆的分工
解释完原始类型,回到开头那个让人困惑的现象:
js
let obj1 = { name: '张三' };
let obj2 = obj1;
obj2.company = '快手';
console.log(obj1, obj2);
// 输出:
// { name: '张三', company: '快手' } { name: '张三', company: '快手' }
为什么改 obj2 会把 obj1 也改了?答案在内存里。
4.1 冯诺依曼架构:代码跑起来之后
现代计算设备都遵循冯诺依曼架构 ,由五部分组成:运算器、控制器、存储器、输入设备、输出设备。
你写的 .js 文件存在硬盘(外存)上。执行时:
- 编译:代码从硬盘调入内存
- JS 引擎创建执行上下文(变量环境 + 词法环境 + 可执行代码)
- 执行上下文被推入调用栈(栈内存)
调用栈就是 JS 代码执行的地方。为什么执行上下文放在栈里? 因为函数执行上下文占用的空间是算得出来的------编译阶段就能确定需要多少内存,刚好合适。当一个函数执行完出栈,引擎只需要做一次指针偏移量切换 就能跳到新的栈顶,继续执行下一个上下文。所以栈的特点是:快速、稳定、可扩展。
但栈和堆,分工不同:
| 维度 | 栈(Stack) | 堆(Heap) |
|---|---|---|
| 用途 | 存原始类型值 + 引用地址 | 存对象本身 |
| 大小 | 小,固定 | 大,动态分配 |
| 速度 | 快(偏移量切换) | 慢(需要寻址) |
| 生命周期 | 函数执行完自动回收 | 由垃圾回收器管理 |
4.2 原始类型 vs 引用类型的赋值行为
ini
原始类型赋值(拷贝式):
let a = null;
let b = a; // 在栈上复制了一份 null
b = 2; // 改的是 b 自己的那份,a 不受影响
引用类型赋值(引用式):
let obj1 = { name: '张三' };
// 栈上存的是地址 0x001,指向堆上的 { name: '张三' }
let obj2 = obj1;
// 栈上复制了同一个地址 0x001,两个变量指向堆上的同一个对象
obj2.company = '快手';
// 沿着 0x001 找到堆上的对象,修改它------obj1 也指向它,所以 obj1 也"变了"
原始类型在栈上直接写值,拷贝时就真的"复制了一份"。引用类型在栈上只存了一个地址牌,拷贝时复制的是地址牌------两个牌子指向堆上的同一个房子。从一个门进去改了装修,另一个门进去看到的一样变了。
5. 如果只记住三件事
- 8 种类型,7 原 1 引 --- Number、String、Boolean、null、undefined、Symbol、BigInt 是原始类型,存的是值本身;Object(含数组、函数)是引用类型,存的是地址。
- null 是你设的,undefined 是引擎给的 --- 不混用就能避开大半类型相关的 bug。
- 拷贝和引用,取决于栈上存的是值还是地址 --- 原始类型赋值是"复印机",引用类型赋值是"同一个房子的两把钥匙"。
下一篇讲类型判断:
typeof、instanceof、Object.prototype.toString.call()------为什么typeof null === 'object'?以及如何准确判断数组、函数、和自定义对象类型。