从 Number.MAX_VALUE 探秘 JavaScript 世界的神秘数字

1,228 阅读10分钟

1.7976931348623157e+308,这个神秘数字是 JavaScript 能够表示的最大数字。今天我们从这个神秘数字出发,从 IEEE 754 标准推导这些神秘数字是如何计算的。今天出现的神秘数字有 1.7976931348623157e+3085e-32490071992547409912.220446049250313e-160.30000000000000004

Number.MAX_VALUE

JavaScript 的 Number 对象中存储了很多常量,神秘数字 1.7976931348623157e+308 就在其中,打开浏览器 Console,输入 Number.MAX_VALUE,就会得到这个数字:

iShot2021-09-14 15.23.09.png

1.7976931348623157e+308 也就是 1.7976931348623157103081.7976931348623157 * 10^{308}

我们今天就来探究这个数字到底是怎么来的。

JavaScript 使用的是 IEEE 754 标准定义的 64 位浮点数,也叫做双精度浮点数。IEEE 754 的 64 位,由三部分组成,分别是:

  1. 符号位(sign bit):1 bit
  2. 指数部分(exponent bias):11 bit
  3. 尾数部分(fraction): 52 bit

iShot2021-09-14 15.29.58.png

490px-General_floating_point_frac.svg.png

我们先看看指数部分,指数一共是 11 位,如果全部为 1,则最大能够表示 2111=20472^{11} - 1 = 2047。所以指数的范围是 [0, 2047]。但是指数部分有负数,所以定义了一个偏移量,在 64 位浮点数中,偏移量为 1023( 2e12^e - 1ee1111)。减去偏移量之后,指数的范围变成了 [-1023, 1024]

但是指数全为 1 和全为 0 有特殊作用,所以我们可用的指数少了 -1023(对应指数全 0)和 1024(对应指数全 1),范围变成了 [-1022, 1023]。

指数不全为 1 且指数不全为 0 的浮点数称作规约化浮点数

我们知道 10 进制的科学计数法中,如 1.7976931348623157103081.7976931348623157 * 10^{308},小数点前的数字一定是大于 0 的。对于二进制而言也一样,二进制小数点前数字必须大于 0,而二进制世界只有 0 和 1,所以二进制的科学计数法小数点前的数字一定是1,这样我们就可以节省 1 位,52 位尾数部分可以全部用来表示小数点后面数字。

综上,64 位规约化浮点数的公式是这样的:

1sign×(1.F)2×2E1023-1^{sign} \times (1.F)_{2} \times 2^{E-1023}

目前已知的条件就可以求出咱们的神秘数字了,想要最大值,指数部分取最大值 1023,尾数全是 1 的话最大,所以我们最大的数字应该是这样的:

iShot2021-09-14 16.23.03.png

我们代入公式,其中 sign 为 0,F 全为 1,E 为 2046:

1sign×(1.F)2×2E1023-1^{sign} \times (1.F)_{2} \times 2^{E-1023}

iShot2021-09-14 16.32.13.png

我们用 JavaScript 来验证一下这个值:

(2 ** 53 - 1) * (2 ** 971) // 1.7976931348623157e+308
Number.MAX_VALUE === (2 ** 53 - 1) * (2 ** 971) // true

D5D1CF3A-1A15-4EC4-B43E-5F5327D42EA5.png

没问题,1.7976931348623157e+308 这个神秘数字我们终于计算了出来。

刚才没有提符号位,符号位非常简单,0 表示正数,1 表示负数。

特殊值 0,Infinity,NaN

刚才提到了,指数部分全为 1 或者全为 0 会有特殊作用,我们先来看看 3 组特殊值。

0:指数位全 0,尾数位也全是0,则表示 ±0

iShot2021-09-14 16.57.12.png

:指数全 1,尾数全 0,则表示 ±∞,也就是 Number.POSITIVE_INFINITYNumber.NEGATIVE_INFINITY

iShot2021-09-14 16.59.29.png

NaN:指数全1,尾数不全为 0,则表示非数字 NaN

iShot2021-09-14 17.00.16.png

Number.MIN_VALUE 和非规约数

我们来看一个相对正常的数字 5e-324,这是 Number.MIN_VALUE 的值:

iShot2021-09-14 17.03.50.png

按照上文规约化浮点数的公式,

1sign×(1.F)2×2E1023-1^{sign} \times (1.F)_{2} \times 2^{E-1023}

规约化浮点数,指数部分范围 [-1022, 1023]。最小值 E = 1,指数部分为 -1022,尾数部分全为0最小,此时最小值为:

iShot2021-09-14 18.14.00.png

我们用 JavaScript 来验证一下这个值:

2**(-1022) // 2.2250738585072014e-308
Number.MIN_VALUE < 2**(-1022) // true

显然,规约化浮点数的最小值 2.2250738585072014e-308 远大于 5e-324,从已知的信息,我们是无论如何也推导不出 5e-324 的,因为 IEEE 754 还定义了一种特殊的类型,非规约数(denormalized number),这类数字指数部分全为 0,尾数部分不全为 0。

需要特别注意的是,非规约数中,偏移量比规约数偏移量小 1,64 位非规约浮点数偏移量为 10231=10221023 - 1 = 1022

公式如下:

1sign×(0.F)2×2E1022-1^{sign} \times (0.F)_{2} \times 2^{E-1022}

由于指数部分全为 0,E 为 0,所以指数部分为 -1022,上述公式简化为:

1sign×(0.F)2×21022-1^{sign} \times (0.F)_{2} \times 2^{-1022}

从公式可以看出,我们可以用非规约数表示更接近 0 的数字。那么我们来看看最小值:指数始终为 -1022,若想要最小,则尾数部分末尾只有 1 个 1 是最小的,如下图所示:

iShot2021-09-14 17.27.24.png

我们代入公式

iShot2021-09-14 18.18.22.png

再来用 JavaScript 来验证一下这个值:

2**(-1074) // 5e-324
Number.MIN_VALUE === 2**(-1074) // true

iShot2021-09-14 18.20.27.png

终于,这个看似正常的 5e-324 是通过不那么正常的公式推导出来的。

小结

上文从求 1.7976931348623157e+308 的思路出发,对 Number.MAX_VALUENumber.MIN_VALUE 进行推导,总结如下:

我们可以把 64 位浮点数分为 3 类:

1、特殊值

  • 0:指数位全 0,尾数位也全是 0,则表示 ±0
  • ∞:指数全 1,尾数全 0,则表示 ±∞
  • NaN:指数全 1,尾数不全为 0,则表示非数字 NaN

2、规约形式的浮点数

指数位不全为 0,且不全为 1,此时偏移量为 1023,指数范围 [-1022, 1023]

1sign×(1.F)2×2E1023-1^{sign} \times (1.F)_{2} \times 2^{E-1023}

3、非规约形式的浮点数

指数位全 0,尾数不全为 0,此时偏移量为 1022,指数部分只为 -1022

1sign×(0.F)2×21022-1^{sign} \times (0.F)_{2} \times 2^{-1022}

还有谁

其实还有几个神秘数字,有了上面的公式,我们都能够推导出来,我们一个个看:

最大安全整数 Number.MAX_SAFE_INTEGER

Number.MAX_SAFE_INTEGER 的值是 9007199254740991,我们分析一下,规约化浮点数,尾数部分有 52 位,最大安全整数应该是小数部分全为 1,指数部分为 52:

iShot2021-09-15 09.07.40.png

用 JavaScript 来验证一下

2**53 - 1 // 9007199254740991
Number.MAX_SAFE_INTEGER === 2**53 - 1 // true

没问题,这个神秘数字 9007199254740991 就是 25312^{53} -1

来看看为什么这个数字是最大安全整数,因为如果比这个数更大,尾数位已经全部是 1 了,只能增大指数,所以比 Number.MAX_SAFE_INTEGER 更大的整数是:

iShot2021-09-15 09.11.22.png

Number.MAX_SAFE_INTEGER 的 2 倍,所以最大安全整数只能是 9007199254740991

还有一个数字 Number.MIN_SAFE_INTEGER,值为 -9007199254740991,这个就很简单,符号位变为 1,也就是:

Number.MIN_SAFE_INTEGER === - Number.MAX_SAFE_INTEGER // true

最小精度 Number.EPSILON

我们来看看最后一个神秘数字 Number.EPSILON2.220446049250313e-16 是如何来的。

Number.EPSILON 属性表示 1 与 Number 可表示的大于 1 的最小的浮点数之间的差值。可表示大于 1 的最小浮点数是这样的:

iShot2021-09-15 09.24.46.png

那么根据定义, Number.EPSILON 就是:

iShot2021-09-15 09.33.17.png

用 JavaScript 来验证一下:

2**-52 // 2.220446049250313e-16
Number.EPSILON === 2**-52 // true

没问题,最后一个神秘数字搞定, 2.220446049250313e-16 就是 2522^{-52}

回到那道经典题目 “0.1 + 0.2 为什么等于 0.30000000000000004”

十进制小数转二进制

先回顾一下十进制小数转 2 进制方法:“乘2取整,顺序排列”法:

0.1 转换二进制:

iShot2021-09-15 10.55.47.png

0.2 转换二进制:

iShot2021-09-15 10.25.41.png

可以看到,0.1 和 0.2 转为二进制都是无限循环小数,转为 64 位浮点数会有精度损失,我们来转换一下:

0.1 在 64 位浮点数中的存储

iShot2021-09-15 11.01.07.png

使用 (1019).toString(2) 可以算出 1019 的二进制为 1111111011

iShot2021-09-15 11.14.00.png

共 10 位,头部补 0 得到 11 位指数 01111111011

iShot2021-09-15 11.16.53.png

再来看尾数部分:

iShot2021-09-15 11.17.36.png

1 开始,0111 循环,到了第 52 位为 1,但是需要额外注意,第 53 位仍然是 1,舍去需要进 1,尾数部分变为了(为了方便阅读,使用了 ES2021 的数值分隔符1_0011_0011_0011_0011_0011_0011_0011_0011_0011_0011_0011_0011_010

iShot2021-09-15 11.19.41.png

因此,0.1 在 64 位浮点数上存储如下:

iShot2021-09-15 11.21.32.png

0.2 在 64 位浮点数中的存储

iShot2021-09-15 11.35.57.png

使用 (1020).toString(2) 可以算出 1020 的二进制为 1111111100

iShot2021-09-15 11.37.09.png

共 10 位,头部补 0 得到 11 位指数 01111111100

iShot2021-09-15 11.38.15.png

尾数部分和 0.1 完全一致,也需要进 1,尾数部分为 1_0011_0011_0011_0011_0011_0011_0011_0011_0011_0011_0011_0011_010。因此 0.2 在 64 位浮点数上存储如下:

iShot2021-09-15 11.59.38.png

浮点数加法

现在需要这两个数字相加,但是指数不一致,没有办法直接相加,需要转换,这次转换带来了第二次精度损失

指数不一致,需要将较小的指数调整和较大的指数一致,在本例中,需要将 0.1 指数位调整到 1020,因此尾数位需要右移,注意规约数小数点前的 1 也要右移,变为尾数部分变为 11_0011_0011_0011_0011_0011_0011_0011_0011_0011_0011_0011_0011_01

iShot2021-09-15 13.00.20.png

现在指数部分相同,我们把尾数部分相加:

(0b1_0011_0011_0011_0011_0011_0011_0011_0011_0011_0011_0011_0011_010 
+ 0b11_0011_0011_0011_0011_0011_0011_0011_0011_0011_0011_0011_0011_01
).toString(2)

得到结果 10110011001100110011001100110011001100110011001100111 ,共 53 位。 这块需要特别注意,规约数小数点左侧默认为 1,现在加法之后多出一位,小数点左侧 +1,变为了 (10)2(10)_2, 可以理解为 (10.0110011001100110011001100110011001100110011001100111)2(10.0110011001100110011001100110011001100110011001100111)_2 这个数字。

小数点需要左移动,指数 +1,变为 1021,尾数需要舍去 1 位,由于尾数为 1,需要进 1,代入公式:

1.0011001100110011001100110011001100110011001100110100210211023=100110011001100110011001100110011001100110011001101002252=100110011001100110011001100110011001100110011001101002541.0011001100110011001100110011001100110011001100110100 * 2^{1021 - 1023} \\= 10011001100110011001100110011001100110011001100110100* 2^{-2 - 52} \\= 10011001100110011001100110011001100110011001100110100* 2^{-54}

用 JavaScript 验证:

0b10011001100110011001100110011001100110011001100110100 * (2**-54) // 0.30000000000000004
0b10011001100110011001100110011001100110011001100110100 * (2**-54) === 0.1 + 0.2 // true

没问题,验证结束。

参考资料