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 会保留一部分给特殊值(0InfinityNaN),真正可用的范围覆盖了足够宽的数值区间。

重点提醒:范围大,≠ 每个整数都能精确表示。


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 只能被近似成最近的可表示数:810

结果:可表示的数变成 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**532**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 “安全整数"的含义

“安全"不是说超过就完全不能算,而是两个条件都满足:

  1. 这个数本身能被精确表示
  2. 这个数前后相邻的整数也能被精确区分(不会出现 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 的 Number64 位浮点数,不是 64 位整数。
  • 浮点数只有 53 位有效精度,超过 2⁵³ − 1 之后,整数之间的间隔会逐渐扩大(2、4、8……),像 2**53 + 1 这样的数因为没有对应的精确存储位置,会被自动舍入到最近的可表示值。
  • 如果需要处理超大整数,请使用 BigInt