学 JavaScript 数据类型,真正要搞懂的是:变量里存的到底是什么?
最近复习了 JavaScript 的数据类型、
null、undefined、堆栈内存、BigInt和Symbol。这些知识看起来很基础,但它们决定了我们能不能真正理解赋值、对象修改、精度问题和垃圾回收。
前言
刚开始学 JavaScript 数据类型时,很容易只停留在背概念:
txt
Number、String、Boolean、null、undefined、Symbol、BigInt、Object
但真正写代码时,问题往往不是"JS 有几种类型",而是:
txt
为什么改了 obj2,obj1 也变了?
为什么 0.1 + 0.2 不等于 0.3?
null 和 undefined 到底怎么区分?
为什么 Symbol("a") 不等于 Symbol("a")?
这些问题背后,其实都指向同一件事:变量里保存的到底是什么。
JavaScript 有哪些数据类型?
JavaScript 数据类型可以分成两大类:
- 原始类型
- 引用类型
原始类型有 7 种:
txt
Number
String
Boolean
null
undefined
Symbol
BigInt
引用类型主要是:
txt
Object
数组、函数、日期、正则,本质上也都属于对象类型的不同形式。
所以可以简单记成:
txt
7 种原始类型 + 1 种引用类型 = 8 种数据类型
原始类型:赋值拷贝的是值
先看一个简单例子:
js
let a = null;
let b = a;
b = 2;
console.log(a); // null
console.log(b); // 2
这里 b = a 的意思是,把 a 里面保存的值复制一份给 b。
后面修改 b,不会影响 a。
这就是原始类型的特点:
txt
赋值时,拷贝的是值本身。
类似 Number、String、Boolean、null、undefined、Symbol、BigInt 都可以先这样理解。
引用类型:赋值拷贝的是地址
再看对象:
js
let obj1 = { name: "雷神" };
let obj2 = obj1;
obj2.company = "快手";
console.log(obj1);
console.log(obj2);
你会发现,obj1 里面也出现了 company。
原因是:
js
let obj2 = obj1;
这行代码并没有复制一个新对象,而是复制了对象的引用地址。
也就是说,obj1 和 obj2 指向的是同一个对象。
可以这样理解:
txt
obj1 里保存一个地址
obj2 拷贝了这个地址
两个变量通过同一个地址找到同一个对象
所以修改其中一个变量指向的对象,另一个变量也能看到变化。
这就是引用类型和原始类型最关键的区别。
null:我主动告诉你这里是空
null 通常表示"这里本来可以有值,但现在明确为空"。
比如:
js
let user = {
name: "Alice",
address: null
};
console.log(user.address); // null
这里的 address: null 表达的是:这个字段存在,只是目前没有地址。
再比如手动解除引用:
js
let largeObject = {
data: new Array(10).fill("hgh")
};
largeObject = null;
这表示变量 largeObject 不再指向原来的对象。
注意,这不是立即释放内存,而是让原来的对象有机会被垃圾回收机制回收。
undefined:系统告诉你这里还没有值
undefined 更像是系统给出的默认结果。
常见场景有几个。
声明变量但没有赋值:
js
let a;
console.log(a); // undefined
访问不存在的对象属性:
js
let obj = {};
console.log(obj.age); // undefined
函数没有返回值:
js
function noReturn() {}
console.log(noReturn()); // undefined
访问不存在的数组索引:
js
let arr = [1, 2, 3];
console.log(arr[5]); // undefined
可以简单区分:
txt
null:我主动设置为空
undefined:系统发现这里没有值
这句话对初学阶段很有用。
栈内存和堆内存:变量保存在哪里?
理解原始类型和引用类型,还需要一点内存模型。
可以先建立一个简化版本:
txt
栈内存:存基本值、引用地址、函数执行上下文
堆内存:存对象、数组、函数等复杂数据
比如:
js
let num = 1;
let obj = { name: "Alice" };
可以粗略理解为:
num的值1直接放在栈里obj变量里保存的是一个地址- 真正的
{ name: "Alice" }对象放在堆里
这也解释了为什么对象赋值后会互相影响。
因为两个变量保存了同一个堆内存地址。
Number:为什么 0.1 + 0.2 不等于 0.3?
看这段代码:
js
let a = 0.1;
let b = 0.2;
console.log(a + b);
结果不是精确的 0.3,而是:
txt
0.30000000000000004
原因是计算机底层用二进制表示数字,而有些十进制小数无法被二进制精确表示。
这不是 JavaScript 独有的问题,很多语言都有类似现象。
在金额计算、精度要求高的场景里,不能直接依赖浮点数做精确计算。
BigInt:用来处理大整数
普通 Number 有安全整数范围。如果数字特别大,就需要 BigInt。
写法是在整数后面加 n:
js
let num1 = 999999999999999999999999999999999999999n;
let num2 = 123456789098765433467324577654789008733n;
console.log(num1 + num2);
console.log(typeof num1); // bigint
需要注意的是,BigInt 不能随便和普通 Number 混用。
正确:
js
console.log(num1 + 1n);
不推荐直接这样混:
js
// TypeError
console.log(num1 + 1);
如果要一起运算,需要先明确转换类型。
Symbol:每次创建都是唯一的
Symbol 用来创建唯一值。
即使描述文字一样,两个 Symbol 也不相等:
js
console.log(Symbol("雷神") === Symbol("雷神")); // false
console.log(typeof Symbol("雷神")); // symbol
Symbol("雷神") 里的 "雷神" 只是描述,不参与相等判断。
它常用于创建不容易冲突的对象属性:
js
let obj = {
[Symbol()]: "value",
prop: "2"
};
普通对象属性名很容易重复,但 Symbol 创建出来的属性键是唯一的。
小结
这次复习 JS 数据类型,最重要的不是背名字,而是理解变量里保存的内容。
核心点可以压缩成几句话:
- JS 有 7 种原始类型和 1 种引用类型
- 原始类型赋值拷贝值
- 引用类型赋值拷贝地址
null是主动设置为空undefined通常表示还没有值- 栈内存保存基本值和引用地址
- 堆内存保存对象、数组、函数等复杂数据
Number存在浮点数精度问题BigInt适合处理大整数Symbol用来创建唯一标识
最后,用一句话总结:
txt
学 JS 数据类型,不只是学类型名字,而是学变量和值之间的关系。
理解了这个关系,再去看浅拷贝、深拷贝、垃圾回收、类型判断和隐式转换,就会顺很多。