1. 为什么整数安全范围是 ±2⁵³,而不是 ±2⁶³?
因为 JavaScript 里的 Number 不是整数类型,而是 64 位双精度浮点数(IEEE 754 double)。
这两种"64 位"的本质区别是:
- 64 位整数:64 位全部用来存整数值,每一位都直接贡献数值
- 64 位浮点数:64 位要被拆成三段(符号 + 指数 + 尾数),真正用来表示精度的位数远少于 64 位
JavaScript 用的是后者,所以它的安全整数范围做不到 ±2⁶³。
2. IEEE 754 双精度浮点数的结构
2.1 三部分组成
一个 Number 占 64 位,按固定格式划分为三段:
| 部分 | 位数 | 作用 |
|---|---|---|
| 符号位 | 1 位 | 表示正负 |
| 指数位 | 11 位 | 决定数的"量级"(数有多大或多小) |
| 尾数位 | 52 位 | 决定数的"精细程度" |
一句话概括:指数位负责移动小数点,尾数位负责记录具体数值。
2.2 二进制科学计数法
十进制有科学计数法:
1234000 = 1.234 × 10^6
二进制也有类似写法:
101.101₂ = 1.01101 × 2^2
IEEE 754 存的正是这种形式:
(-1)^符号位 × 1.尾数 × 2^指数
注意最前面的 1. 是隐含的,默认存在、不占位,这也是为什么尾数只有 52 位,但有效精度却是 53 位。
2.2.1 指数决定"范围"
指数越大,整个数就越大:
1.0 × 2^0 = 1
1.0 × 2^10 = 1024
1.0 × 2^53 = 非常大的数
2.2.2 尾数决定"精度"
尾数位越多,越能分辨出两个相邻的数。尾数位有限,能精确区分的数就有上限。
3. 指数位的作用
3.1 同样的尾数,指数不同 → 数值完全不同
尾数 1.0101 在不同指数下的结果:
1.0101 × 2^1 = 2.625
1.0101 × 2^5 = 42
1.0101 × 2^20 = 很大的数
所以指数位不是精度位,它是放大倍数。
3.2 为什么浮点数能同时表示极大和极小的数?
因为指数可正可负:
- 指数大 → 表示很大的数(如
1e308) - 指数小(负数)→ 表示很小的数(如
1e-308)
这就是"浮点"的含义:小数点的位置不是固定的,而是随指数浮动的。
3.3 11 位指数的能力
11 位指数理论上有 2¹¹ = 2048 种组合,但 IEEE 754 会保留一部分给特殊值(0、Infinity、NaN),真正可用的范围覆盖了足够宽的数值区间。
重点提醒:范围大,≠ 每个整数都能精确表示。
4. 为什么超过 2⁵³ 之后就不精确了?
核心规律:
浮点数的有效位数固定。数越大,这些位数越多地被整数部分"占用",留给细微变化的空间就越少,导致相邻可表示数之间的间隔越来越大。
4.1 “53 位有效精度"从哪来?
尾数栏位有 52 位,但由于最高位那个 1 是隐含的(不需要实际存储),所以总有效精度是:
52 + 1(隐含位)= 53 位有效二进制位
这 53 位,决定了 Number 能精确区分多少个连续整数。
4.2 用"3 位精度"的简化模型来理解
为了不被 53 这个数字绕晕,先假设一个缩小版:这个世界的浮点数只有 3 位有效位。
4.2.1 小范围内:每个整数都有位置
1 = 1.00 × 2^0
2 = 1.00 × 2^1
3 = 1.10 × 2^1
4 = 1.00 × 2^2
5 = 1.01 × 2^2
6 = 1.10 × 2^2
7 = 1.11 × 2^2
每个整数都能精确表示,相邻间隔 = 1。
4.2.2 到了 8:刻度开始变粗
8 = 1000₂ = 1.000 × 2^3 ✅ 能表示
9 = 1001₂ = 1.001 × 2^3 ❌ 需要 4 位尾数,存不下
9 只能被近似成最近的可表示数:8 或 10。
结果:可表示的数变成 8, 10, 12, 14 …,间隔从 1 变成了 2。
4.2.3 再往大走:间隔不断翻倍
数越大,指数越大,尾数能表达的细节越粗:
数值范围 可表示数 间隔
4 ~ 8 4, 5, 6, 7, 8 1
8 ~ 16 8, 10, 12, 14, 16 2
16 ~ 32 16, 20, 24, 28, 32 4
4.3 映射回 JavaScript 的真实情况(53 位)
把刚才的"3 位"换成真实的 53 位:
| 数值范围 | 相邻间隔 |
|---|---|
| 0 ~ 2⁵³ | 1(每个整数都可精确表示) |
| 2⁵³ ~ 2⁵⁴ | 2 |
| 2⁵⁴ ~ 2⁵⁵ | 4 |
| 以此类推… | 每上一个量级,间隔翻倍 |
验证:
2**53 // 9007199254740992 ✅
2**53 + 1 // 9007199254740992 ❌(被舍入,和上面一样)
2**53 + 2 // 9007199254740994 ✅
5. “被舍入"是什么意思?
5.1 生活中的舍入
保留两位小数:
3.14159 → 3.14
3.146 → 3.15
精确值存不下,就改成"最近的能表示的值”,这就是舍入。
5.2 浮点数里的舍入
浮点数保留的不是两位小数,而是固定的二进制位数。
当你写 2**53 + 1,JS 会发现这个精确值存不下,于是找最近的可表示数——恰好是 9007199254740992,也就是 2**53 本身。
2**53 + 1 === 2**53 // true
这不是"算错了”,而是精确值无处安放,被近似成了旁边能表示的值。
5.3 为什么不舍入成 2⁵³ + 2?
2**53 + 1 正好在 2**53 和 2**53 + 2 的正中间,按照 IEEE 754 的"就近舍入"规则(ties to even),这种居中情况会落向偶数那一侧,结果是 2**53。
6. 用"刻度尺"直观理解浮点数精度
把浮点数想象成一把刻度尺:
6.1 小数值区域:刻度很密
..., 1, 2, 3, 4, 5, ...
(每格 = 1,每个整数都有刻度线)
6.2 大数值区域:刻度变稀
..., 9007199254740992, 9007199254740994, 9007199254740996, ...
(每格 = 2,奇数的刻度线消失了)
不是"整数不能表示",而是:太大的整数,刻度不够密,没法逐个安置。
7. 为什么安全整数上限是 2⁵³ − 1,而不是 2⁵³?
7.1 “安全整数"的含义
“安全"不是说超过就完全不能算,而是两个条件都满足:
- 这个数本身能被精确表示
- 这个数前后相邻的整数也能被精确区分(不会出现
n === n + 1)
7.2 安全整数的范围
Number.MIN_SAFE_INTEGER // -9007199254740991,即 -(2⁵³ - 1)
Number.MAX_SAFE_INTEGER // 9007199254740991,即 (2⁵³ - 1)
2⁵³ 本身虽然还能被表示,但它已经站在"刻度变粗"的边界上——再往后加 1 就不安全了,所以上限取 2⁵³ - 1。
8. 64 位整数 vs. 64 位浮点数
8.1 64 位整数
64 位全部用来存整数,1 位做符号,剩下 63 位直接表示数值,所以能精确覆盖 ±2⁶³ 的范围,每个整数都有位置。
8.2 64 位浮点数
64 位要按 1 + 11 + 52 拆分,是在"范围"和"精度"之间做了折中:
- 范围极大(能到 ±1.8 × 10³⁰⁸)
- 但不是每个整数都能逐一精确表示
9. 需要更大的整数精度怎么办?用 BigInt
9.1 BigInt 的特点
BigInt 是 JavaScript 专门用来处理大整数的类型,没有精度丢失问题:
2n ** 63n // 精确,没有任何近似
数字后面加 n 就是 BigInt 字面量。
9.2 BigInt 的限制
BigInt 不能和普通 Number 直接混合运算:
1n + 1 // ❌ TypeError
1n + 1n // ✅ 2n
10. 完整对比示例
10.1 在 2⁵³ 以内:完全正常
9007199254740990 + 1 // 9007199254740991 ✅
9007199254740991 + 1 // 9007199254740992 ✅
10.2 超过 2⁵³:开始出现跳格
9007199254740992 + 1 // 9007199254740992 ❌(+1 消失了)
9007199254740992 + 2 // 9007199254740994 ✅(间隔已变为 2)
11. 一句话总结
- JavaScript 的
Number是 64 位浮点数,不是 64 位整数。 - 浮点数只有 53 位有效精度,超过 2⁵³ − 1 之后,整数之间的间隔会逐渐扩大(2、4、8……),像
2**53 + 1这样的数因为没有对应的精确存储位置,会被自动舍入到最近的可表示值。 - 如果需要处理超大整数,请使用
BigInt。