5. 四类常量

以 JavaScript 语言里的 Number 数据类型为例

在 JavaScript 里,Number 对象有四类静态属性,分别是:

  1. 最大/最小正数

    • 能表示的最大正数:Number.MAX_VALUE

    • 能表示的最小正数(最接近 0 的正数):Number.MIN_VALUE

  2. 最大/最小安全整数

    • 最大安全整数 +(2531)+(2^{53} - 1)Number.MAX_SAFE_INTEGER

    • 最小安全整数 (2531)-(2^{53} - 1)Number.MIN_SAFE_INTEGER

  3. 最小间隔:能表示的数值之间的最小间隔,Number.EPSILON

  4. 特殊值

    • 正负无穷大(上溢时返回)

      • Number.NEGATIVE_INFINITY

      • Number.POSITIVE_INFINITY

    • 非数值(Not a Number),Number.NaN

    • ±0

它们的值分别是:

// 最大正数和最小正数
Number.MAX_VALUE; // 1.7976931348623157e+308
Number.MIN_VALUE; // 5e-324

// 最大安全整数和最小安全整数
Number.MAX_SAFE_INTEGER; //  9007199254740991 = 2**53-1
Number.MIN_SAFE_INTEGER; // -9007199254740991

// 最小间隔
Number.EPSILON; // 2.220446049250313e-16 = 2**-52

// 特殊值:正负无穷大
Number.POSITIVE_INFINITY; // Infinity
Number.NEGATIVE_INFINITY; // -Infinity
// 特殊值:非数值
Number.NaN; // NaN

结合 Number 在内存中的存储方式,如何理解这些常量的含义和值呢?

  • 符号位,1 位,0 正 1 负

  • 指数,11 位,全 0 和全 1 是为特殊数值保留的

  • 有效数,52 个显式存储位 + 1 个默认前导位,共 53 位

1. 最大正数和最小正数

1.1 最大正数

最大正数 MAX_VALUE 应该是:正数即符号位为 0,11 位指数是“全 1-1”,52 位有效数是“全 1”。如下:

对应的 binary64 内存存储就是 0x7FEFFFFFFFFFFFFF

真实值就是:(1)01.11...11(2)220461023(-1)^0 * 1.11...11_{(2)} * 2^{2046-1023}

  • = 1.11...11(2)210231.11...11_{(2)} * 2^{1023}

  • = 111...11(2)2102352111...11_{(2)} * 2^{1023-52}

  • = 111...11(2)2971111...11_{(2)} * 2^{971}

  • = (2531)2971(2^{53} - 1) * 2^{971}

  • = 1.7976931348623157e+308

  • ≈ 1.8e+308

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

1.2 最小正数

最小正数 MIN_VALUE 应该是:正数即符号位为 0,11 位指数是“全 0”,52 位有效数是“全 0+1”。此时即为次正规数,如下:

对应的 binary64 内存存储就是 0x0000000000000001

真实值就是:(1)00.00...01(2)21022(-1)^0 * 0.00...01_{(2)} * 2^{-1022}

  • = 11(2)21022521 * 1_{(2)} * 2^{-1022-52}

  • = 210742^{-1074}

  • = 5e-324

2**-1074; // 5e-324
Number(2**-1074).toPrecision(70); // '4.940656458412465441765687928682213723650598026143247644255856825006755e-324'
Number.MIN_VALUE; // 5e-324
Number.MIN_VALUE === 2**-1074; // true

2. 最大安全整数和最小安全整数

在介绍最大安全整数和最小安全整数之前,我们先来认识下什么是安全整数。

2.1 安全整数

安全整数是形容数学概念上的一个整数在 JavaScript 里的表示方式。如果说一个整数是安全的,就意味着它能在 JavaScript 中被唯一地表示。

The idea of a safe integer is about how mathematical integers are represented in JavaScript. In the range (−253, 253) (excluding the lower and upper bounds), JavaScript integers are safe: there is a one-to-one mapping between mathematical integers and their representations in JavaScript.

那么,如何理解“被唯一”地表示呢?来看个例子。

当 IEEE 754 binary64 的 53 个有效位全是 1 且都处于整数位置时,即实际指数值是 52,偏正指数值是 52+1023 = 1075 = 10000110011(2)10000110011_{(2)},此时的内存表示如下:

真实值就是:(1)01.11...11(2)252(-1)^0 * 1.11...11_{(2)} * 2^{52}

  • = 1.11...11(2)2521.11...11_{(2)} * 2^{52}

  • = 111...11(2)111...11_{(2)}

  • = 25312^{53} - 1

  • = 9007199254740991

当它加 1 时,值会变成 2532^{53} = 1000...00(2)1000...00_{(2)} = 1.00...000 * 2532^{53} = 1.00...000 * 2532^{53}。注意,末位的 0 之所以被删,是因为内存里的 IEEE 754 binary64 格式的有效位只有 53 个(包括默认的前导位 1),所以需要裁切精度。

当再加 1 时,值会变成 253+12^{53} + 1 = 1000...01(2)1000...01_{(2)} = 1.00...001 * 2532^{53} ≈ 1.00...011 * 2532^{53} = 1.00...01 * 2532^{53}。红色的 1 表示即将被裁掉的精度位,11 表示舍掉了 1 之后进位的 1(即四舍五入式)。

为了方便阅读,我们用表格的形式来描述。十进制那列是从 25312^{53}-1 即 9007199254740991 开始的,每行的值依次加 1。如下:

十进制
二进制
二进制科学记数法
IEEE 754 binary64

9007199254740991

111...11111...11

1.11...11 * 2522^{52}

1.11...11 * 2522^{52}

9007199254740992

1000...001000...00

1.000...00 * 2532^{53}

1.00...000 * 2532^{53}

9007199254740993

1000...011000...01

1.000...01 * 2532^{53}

1.00...011 * 2532^{53}

9007199254740994

1000...101000...10

1.000...10 * 2532^{53}

1.00...010 * 2532^{53}

9007199254740995

1000...111000...11

1.000...11 * 2532^{53}

1.00...101 * 2532^{53}

9007199254740996

100...100100...100

1.00...100 * 2532^{53}

1.00...100 * 2532^{53}

9007199254740997

100...101100...101

1.00...101 * 2532^{53}

1.00...111 * 2532^{53}

9007199254740998

100...110100...110

1.00...110 * 2532^{53}

1.00...110 * 2532^{53}

...

说明:“二进制科学记数法”列中的标红数字,是在存入内存时将会被裁掉的部分,因为小数点后只能存储 52 个有效数字。“IEEE 754 binary64”列中,标红的数字即前一列的内容,标绿的数字就是用了四舍五入(即 0 舍 1 入)的舍入方式裁剪精度后最终存储的有效数字。

我们可以很直观地看到,当二进制有效数的位数大于 53 位时,就得舍弃最后一位,这会导致值不同的两个数值会有相同的 IEEE 754 binary64 表示。这,就是没有“被唯一”地表示。

  • 当指数是 53 时,是 2 个整数共用一个内存存储,因为会舍去末位的 53-52 = 1 位

  • 当指数是 54 时,是 4 个整数共用一个内存储存,因为会舍去末位的 54-52 = 2 位

  • 当指数是 55 时,是 8 个整数共用一个内存储存,因为会舍去末位的 55-52 = 3 位

  • 当指数是 56 时,是 16 个整数共用一个内存储存,因为会舍去末位的 56-52 = 4 位

  • ...

注意:如果是用向 0 舍入的方式(即直接截断),那么共用一个内存存储的整数范围会有所不同,不过不影响大局。

JavaScript 里的安全整数的范围是 [(2531)-(2^{53}-1), +(2531)+(2^{53}-1)],没有包含边界 ±253±2^{53}。虽然 2532^{53} 在内存中是能被唯一表示的,但是考虑到如果是用直接截断的方式,它是会和 253+12^{53}+1 重叠的。所以在 JavaScript 中,一旦精度有被截断,就会被视为是不安全的。如下:

Number.isSafeInteger(2**53 - 1); // true
Number.isSafeInteger(2**53); // false
Number.isSafeInteger(2**53 + 1); // false

所以,在 JavaScript 中,说一个整数是安全的,就意味着它能唯一地表示一个数学意义上的整数,且在存储时没有被舍弃精度。

2.2 最大安全整数

最大安全整数 MAX_SAFE_INTEGER 应该是:正数即符号位为 0,指数的实际值是 52(即内存值为 52+1023 = 1075 = 10000110011(2) ),52 位有效数是“全 1”。如下:

对应的 binary64 内存存储就是 0x433FFFFFFFFFFFFF

真实值就是:(1)01.11...11(2)252(-1)^0 * 1.11...11_{(2)} * 2^{52}

  • = 1.11...1122521.11...11_{2}*2^{52}

  • = 111...11(2)111...11_{(2)}

  • = 2532^{53}-1

  • = 9007199254740991

0b10000110011; // 1075
2 ** 53 - 1; // 9007199254740991
Number.MAX_SAFE_INTEGER; // 9007199254740991
Number.MAX_SAFE_INTEGER === 2 ** 53 - 1; // true

2.3 最小安全整数

最小安全整数 MIN_SAFE_INTEGER 除了符号和最大安全整数 MAX_SAFE_INTEGER 不同之外,其余都一样,即:负数即符号位为 1,指数的实际值是 52(即内存值为 52+1023 = 1075 = 10000110011(2)10000110011_{(2)}),52 位有效数是“全 1”。如下:

对应的 binary64 内存存储就是 0xC33FFFFFFFFFFFFF

真实值就是:(1)11.11...11(2)252(-1)^1 * 1.11...11_{(2)} * 2^{52}

  • = 1.11...112252-1.11...11_{2}*2^{52}

  • = 111...11(2)-111...11_{(2)}

  • = (2531)- (2^{53} - 1)

  • = -9007199254740991

0b10000110011; // 1075
2 ** 53 - 1; // 9007199254740991
Number.MIN_SAFE_INTEGER; // -9007199254740991
Number.MIN_SAFE_INTEGER === -(2 ** 53 - 1); // true

3. 最小间隔

3.1 不均匀的断层

《精度损失》里曾提到浮点数到实数的映射,其中间有覆盖不到的小断层,如下图:

能表示的两个数字之间的“间隔”,就是图中的那些个“小断层”。那么,这些断层是均匀的吗?如果均匀,值是多少?如果不均匀,最小值和最大值分别是多少?

要回答这个问题,我们就得看看二进制到十进制的转换了。在《为什么 toPrecision(17) 能让内存中的数值原形毕露?》里我们是将整数部分和小数部分分开来讨论的,相关结论是:

  • 整数部分,当有 n 个二进制有效位时,可以表示 2n2^n 个十进制数,且它们是个等差数列,差值是 1

  • 小数部分,当有 n 个二进制有效位时,可以表示 2n2^n 个十进制数,且它们是个等差数列,差值是 2n2^{-n},数列范围是 [2n2^{-n}, 1)

在真实的 IEEE 754 binary64 存储中,二进制的那 53 个有效位是可以同时有整数部分和小数部分的,如果再考虑指数,这会让每个有效位上的数字权重(即 2 的几次方)变成流动的。所以显然,两个能表示的数之间的“间隔”是不均匀的。

那间隔的最小值和最大值分别是多少?

3.2 最小间隔和最大间隔

假设现在有 4 个二进制有效位,其中包括 1 个默认前导位和 3 个显式存储位。当默认前导位是 0 时是次正规数,当默认前导位是 1 时是正规数。我们看看在这种情况下,二进制到十进制是一个什么样的对应关系。如下:

格式
二进制
十进制
间隔值

0.xxx

0.000 0.001 0.010 0.011 0.100 0.101 0.110 0.111

0 0.125 0.25 0.375 0.5 0.625 0.75 0.875

0.125 = 232^{-3}

1.xxx

1.000 1.001 1.010 1.011 1.100 1.101 1.110 1.111

1 1.125 1.25 1.375 1.5 1.625 1.75 1.875

0.125 = 232^{-3}

1x.xx

10.00 10.01 10.10 10.11 11.00 11.01 11.10 11.11

2 2.25 2.5 2.75 3 3.25 3.5 3.75

0.25 = 222^{-2}

1xx.x

100.0 100.1 101.0 101.1 110.0 110.1 111.0 111.1

4 4.5 5 5.5 6 6.5 7 7.5

0.5 = 212^{-1}

1xxx

1000 1001 1010 1011 1100 1101 1110 1111

8 9 10 11 12 13 14 15

1 = 202^0

同理,当我们有 53 个二进制有效位时(1 个默认前导位 + 52 个显式位),从 0 ~ 1 ~ 212^1 ~ 222^2 ~ 232^3 ~ 242^4 ~ ... ~ (2532^{53} - 1) 之间,每段都是连续的等差数列,只是差值不同而已。其中,最小的差值是 2522^{-52},最大的差值是 202^0 = 1。

之所以只考虑了次正规数和 0 ≤ 指数实际值 ≤ 52 的情况,是因为当指数过大或者过小时,能表示的数值就不是线性连续的了。感兴趣的小伙伴们,可以自行推导下。

在数轴上,二进制和十进制的逻辑类似。

综上,最小间隔 EPSILON 就是能表示的等差数列的最小差值,即 2522^{-52} = 2.220446049250313e-16

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

4. 特殊值

包括次正规数,还有三类特殊值,它们都是依据指数的特殊编码来区分的。详情可查看《双精度浮点数 / 内存存储 / 指数位编码》,这里就不展开说了。

4.1 正负无穷大

这两个就是指数位“全 1”的特殊情况(之一):指数位是“全 1”,有效数位是“全 0”。

正无穷大的 binary64 的内存存储为 0x7FF0000000000000,如下图:

负无穷大的 binary64 的内存存储是 0xFFF0000000000000,如下图:

Number.POSITIVE_INFINITY === Infinity; // true
Number.NEGATIVE_INFINITY === -Infinity; // true

4.2 非数值

NaN ,Not a Number,是指数位“全 1”的特殊情况(之一):指数位是“全 1”,只要有效数位不是“全0”的都是 NaN(忽略符号位)。如下图:

0x7FF0000000000001 ~ 0x7FFFFFFFFFFFFFFF 都是 NaN

4.3 ±0

0 有两种表示方式:-0 和 +0(0 是 +0 的别名)。在实际开发中,这几乎没啥影响。但需要注意一点,就是当 0 是被除数的时候,会有不同:

33 / 0; // Infinity
33 / -0; // -Infinity
-0 === 0; // 虽然是 true

+0 的 IEEE 754 binary64 内存存储为:

-0 的 IEEE 754 binary64 内存存储为:

5. 总结

这部分以 JavaScript 语言里的数值类型 Number 为例,介绍了双精度浮点数里的四类常量,对它们知其然也知其所以然会对编程大有裨益。

  1. 能表示的最大正数和最小正数

  2. 能表示的最大安全整数和最小安全整数

  3. 能表示的数值之间的间隔

    • 浮点数→实数:有间隔,且间隔不均匀

    • 在线性增长中,是 n 段等差数列。最小差值/间隔是 Number.EPSILON,最大差值/间隔是 1

    • 在指数增长中,安全整数范围内的数列是连续的,之外的是不连续的(有重叠,没被唯一表示)

  4. 特殊值:NaN, ±Infinity, ±0

6. 主要参考

Last updated