探祕 JavaScript 世界的神祕數字 1.7976931348623157e+308

語言: CN / TW / HK

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 ,就會得到這個數字:

1.7976931348623157e+308 也就是

我們今天就來探究這個數字到底是怎麼來的。

JavaScript 使用的是 IEEE 754 標準定義的 64 位浮點數,也叫做雙精度浮點數。IEEE 754 的 64 位,由三部分組成,分別是:

  1. 符號位(sign bit):1 bit

  2. 指數部分(exponent bias):11 bit

  3. 尾數部分(fraction): 52 bit

我們先看看指數部分,指數一共是 11 位,如果全部為 1,則最大能夠表示 。所以指數的範圍是 [0, 2047]。但是指數部分有負數,所以定義了一個 偏移量 ,在 64 位浮點數中,偏移量為 1023( )。減去偏移量之後,指數的範圍變成了 [-1023, 1024]

但是指數全為 1 和全為 0 有特殊作用,所以我們可用的指數少了 -1023(對應指數全 0)和 1024(對應指數全 1),範圍變成了 [-1022, 1023]。

指數不全為 1 且指數不全為 0 的浮點數稱作 規約化浮點數

我們知道 10 進位制的 科學計數法 中,如 ,小數點前的數字一定是大於 0 的。對於二進位制而言也一樣,二進位制小數點前數字必須大於 0,而二進位制世界只有 0 和 1,所以 二進位制的科學技術法小數點前的數字一定是 1 ,這樣我們就可以節省 1 位,52 位尾數部分可以全部用來表示小數點後面數字。

綜上,64 位 規約化浮點數 的公式是這樣的:

目前已知的條件就可以求出咱們的神祕數字了,想要最大值,指數部分取最大值 1023,尾數全是 1 的話最大,所以我們最大的數字應該是這樣的:

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

我們用 JavaScript 來驗證一下這個值:

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

複製程式碼

沒問題, 1.7976931348623157e+308 這個神祕數字我們終於計算了出來。

剛才沒有提符號位,符號位非常簡單,0 表示正數,1 表示負數。

特殊值 0,Infinity,NaN

剛才提到了,指數部分全為 1 或者全為 0 會有特殊作用,我們先來看看 3 組特殊值。

0:指數位 全 0 ,尾數位也 全是 0 ,則表示 ±0

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

NaN:指數 全 1 ,尾數 不全為 0 ,則表示非數字 NaN

Number.MIN_VALUE 和非規約數

我們來看一個相對正常的數字 5e-324 ,這是 Number.MIN_VALUE 的值:

按照上文規約化浮點數的公式,

規約化浮點數,指數部分範圍 [-1022, 1023]。最小值 E = 1,指數部分為 -1022,尾數部分全為 0 最小,此時最小值為:

我們用 JavaScript 來驗證一下這個值:

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

複製程式碼

顯然,規約化浮點數的最小值 2.2250738585072014e-308 遠大於 5e-324 ,從已知的資訊,我們是無論如何也推導不出 5e-324 的,因為 IEEE 754 還定義了一種特殊的型別,非規約數(denormalized number),這類數字 指數部分全為 0 ,尾數部分不全為 0。

需要特別注意的是,非規約數中, 偏移量比規約數偏移量小 1 ,64 位非規約浮點數偏移量為

公式如下:

由於指數部分全為 0,E 為 0,所以指數部分為 -1022,上述公式簡化為:

從公式可以看出,我們可以 用非規約數表示更接近 0 的數字 。那麼我們來看看最小值:指數始終為 -1022,若想要最小,則尾數部分末尾只有 1 個 1 是最小的,如下圖所示:

我們代入公式

再來用 JavaScript 來驗證一下這個值:

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

複製程式碼

終於,這個看似正常的 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]

3、非規約形式的浮點數

指數位全 0,尾數不全為 0,此時偏移量為 1022,指數部分只為 -1022

還有誰

其實還有幾個神祕數字,有了上面的公式,我們都能夠推匯出來,我們一個個看:

最大安全整數 Number.MAX_SAFE_INTEGER

Number.MAX_SAFE_INTEGER 的值是 9007199254740991 ,我們分析一下,規約化浮點數,尾數部分有 52 位,最大安全整數應該是小數部分全為 1,指數部分為 52:

用 JavaScript 來驗證一下

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

複製程式碼

沒問題,這個神祕數字 9007199254740991 就是

來看看為什麼這個數字是最大安全整數,因為如果比這個數更大,尾數位已經全部是 1 了,只能增大指數,所以比 Number.MAX_SAFE_INTEGER 更大的整數是:

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 的最小浮點數是這樣的:

那麼根據定義, Number.EPSILON 就是:

用 JavaScript 來驗證一下:

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

複製程式碼

沒問題,最後一個神祕數字搞定, 2.220446049250313e-16 就是

回到那道經典題目 “0.1 + 0.2 為什麼等於 0.30000000000000004”

十進位制小數轉二進位制

先回顧一下十進位制小數轉 2 進位制方法:“乘 2 取整,順序排列”法:

0.1 轉換二進位制:

0.2 轉換二進位制:

可以看到,0.1 和 0.2 轉為二進位制都是無限迴圈小數,轉為 64 位浮點數會有精度損失,我們來轉換一下:

0.1 在 64 位浮點數中的儲存

使用 (1019).toString(2) 可以算出 1019 的二進位制為 1111111011

共 10 位,頭部補 0 得到 11 位指數 01111111011

再來看尾數部分:

1 開始,0111 迴圈,到了第 52 位為 1,但是需要額外注意,第 53 位仍然是 1,捨去需要進 1,尾數部分變為了(為了方便閱讀,使用了 ES2021 的 數值分隔符1_0011_0011_0011_0011_0011_0011_0011_0011_0011_0011_0011_0011_010

因此,0.1 在 64 位浮點數上儲存如下:

0.2 在 64 位浮點數中的儲存

使用 (1020).toString(2) 可以算出 1020 的二進位制為 1111111100

共 10 位,頭部補 0 得到 11 位指數 01111111100

尾數部分和 0.1 完全一致,也需要進 1,尾數部分為 1_0011_0011_0011_0011_0011_0011_0011_0011_0011_0011_0011_0011_010 。因此 0.2 在 64 位浮點數上儲存如下:

浮點數加法

現在需要這兩個數字相加,但是指數不一致,沒有辦法直接相加,需要轉換,這次轉換帶來了 第二次精度損失

指數不一致,需要將較小的指數調整和較大的指數一致,在本例中,需要將 0.1 指數位調整到 1020,因此尾數位需要右移,注意規約數小數點前的 1 也要右移,變為尾數部分變為 11_0011_0011_0011_0011_0011_0011_0011_0011_0011_0011_0011_0011_01

現在指數部分相同,我們把尾數部分相加:

(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,變為了 ,可以理解為 這個數字。

小數點需要左移動,指數 +1,變為 1021,尾數需要捨去 1 位,由於尾數為 1,需要進 1,代入公式:

用 JavaScript 驗證:

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

複製程式碼

沒問題,驗證結束。

參考資料