前端進階

-

JavaScript 64 位元浮點數最詳細的中文解釋

this.web

你知道嗎?電腦裡的數字和你看到的其實不一樣。

當你在 JavaScript 裡寫 0.13.141000時,電腦會用一種特殊的方式儲存起來,這種方式,就是 IEEE 754 的64 位元浮點數 (double-precision) 格式。

在 JS 中,所有的 Number 型別底層通通都是這種格式。

那什麼是 64 位元浮點數呢?他會有什麼問題嗎?

下面來解釋一下!

什麼是 64 位元浮點數?

所謂64 位元是說,它用 64 個二進位 (bits) 來儲存一個數字。

這 64 位會被分成三部分:

  • 1 位是正負號 (Sign):決定數字的正負。
  • 11 位是指數 (Exponent):決定數字的「縮放倍率」,它讓小數點可以左右移動。
  • 52 位是尾數 (Fraction):決定數字的「精確度」,也就是扣除倍率後實際長什麼樣子。

他會長得像這樣:

正負號    指數           尾數
sign     exponent      Fraction   
+-----+-------------+------------------------------------------------------+
|  0  | 00000000000 | 0000000000000000000000000000000000000000000000000000 |
+-----+-------------+------------------------------------------------------+
1 bit    11 bit                          52 bit
1 個數字  11 個數字                        52 個數字

|__________________________________________________________________________|
                                 64 bit
                                 總共 64 個數字

我們來實際操作看看,比如說 23 這個數字,如果要被儲存為 64 位元的格式,他會有以下步驟:

第一步:把 23 換成二進位

我們先把十進位的 23 變成二進位:

使用 “除 2 取餘法” 可以算出 23 的二進位是:10111

”除 2 取餘法” 具體怎麼算就請大家上網查啦!很簡單的。

第二步:把它變成「科學記號」

電腦儲存時,會把二進位變成 1.xxxxx 的形式。

所以要將 10111 往左移 4 位,變成:1.0111 × 2⁴

  • 11 bits 的指數:用來存這個 42⁴ 的 4)。
  • 52 bits 的尾數:用來存小數點後面的 0111

第三步:這 11 bits 具體怎麼存這個「4」?

這 11 bits 的位置不是直接存 4,它要做一個「平移」,偏移值(Bias)會是 1023。

所以實際存入的數會是:4 + 1023 = 1027。

我們在將 1027 轉成二進位得到 10000000011 後,就是 11 bits 實際儲存的內容。

為什麼要偏移呢?這是為了讓電腦好計算數字的大小!

我們用一個簡化版的 4 位元指數 來做實驗,這樣比較好觀察:

如果不使用偏移,使用一般正負號表示:

  • 指數 +1 的二進位是:0001
  • 指數 -1 的二進位是:1001(第 1 位是符號,後 3 位是數值 1)

這會導致在比大小時很麻煩,因為 1001 數字上比 0001 還大。

但如果我們用偏移,同樣用 4 位元為例(偏移值為 7):

那指數 +1 的二進位會是:1000,而指數 -1 的二進位是:0110

這樣判定大小就非常容易了!

第四步:最後 64 位元長怎樣?

我們把這三個零件拼起來:

部分 長度 內容 說明 正負號 1 bit 0 代表正數 指數 11 bits 10000000011 代表 2^4(1027 - 1023 = 4) 尾數 52 bits 01110...0 存小數點後的 0111,後面補 48 個 0

+-----+-------------+------------------------------------------------------+
|  0  | 10000000011 | 0111000000000000000000000000000000000000000000000000 |
+-----+-------------+------------------------------------------------------+
1 bit    11 bit                          52 bit

有點小複雜對吧?我們前端工程師有必要知道這些嗎?

事實上是有的哦!

64 位元對前端工程師有什麼影響?

居然我們能精準表示的數字只有 52bits,而最大的偏移量只有 1023(2^10 - 1),那超過這個範圍會怎麼樣呢?

當然是直接出事啦!

在 JS 裡的 Number 中有兩個數值,分別是

  • Number.MAX_SAFE_INTEGER:值為 9007199254740991(2^53 - 1)。
  • Number.MAX_VALUE:大約為 1.79 * 10^308(1.7976931348623157e+308)。

1. 精度極限:Number.MAX_SAFE_INTEGER (2^53−1)

為什麼是 2^53−1?:雖然記憶體只存 52 位元尾數,但 IEEE 754 標準預設前面還有一個的 1 (前面第二步中 1.xxxxx 的 1),所以總共有 53 位元的精度。

超過之後計算就會失真、不安全,所以在 JS 裡,

你會發現 9007199254740991 + 19007199254740991 + 2 的結果跟 +1 一樣

精度極限展示

2. 範圍極限:Number.MAX_VALUE (1.79 * 10^308)

這個數字與「11 位元指數」決定的最大位移量(1023 次方)相關。

公式大約是 (2 - 2^-52) * 2^1023 ~= 1.79 * 10^308

這個數字的計算也已經非常不精準的,比如我對 MAX_VALUE 和 MAX_VALUE + 1 作比較,會發現結果是 2 個一樣大

範圍極限展示

3. 小數運算的失真

相信有點經驗的工程師,一定遇過 0.1 + 0.2 = 0.30000000000000004 的問題

小數運算失真

這就是因為十進位的小數十進位的小數在轉換為二進位時,有時候沒辦法精準表示,會變成「無限循環小數」,而電腦的儲存空間(52 位元尾數)是有限的,這導致了微小的捨入誤差。

我們可以把這個過程拆解為三個步驟:

  1. 十進位轉二進位 當我們在十進位寫下 0.1 時,這是一個簡短的數字。但在二進位中,許多十進位小數無法被精確表示。
    • 十進位的 0.1:在二進位中會變成 0.0001100110011...(0011 無限循環)。
    • 十進位的 0.2:在二進位中則是 0.001100110011...(0011 無限循環)。
  2. 52 位元的限制 因為 IEEE 754 64 位元格式只給了尾數 52 個位元的空間,電腦無法儲存無限循環的數字。
    • 電腦必須在第 52 位元的地方進行四捨五入(或捨入到最接近的偶數)
    • 因此,存進記憶體的 0.1 其實並不是真正的 0.1,而是一個極其接近的近似值。
  3. 近似值相加的結果 當你執行 0.1 + 0.2 時,電腦實際上是在做兩個「不精準的近似值」相加:
    1. 電腦取出儲存的 0.1 近似值
    2. 電腦取出儲存的 0.2 近似值
    3. 將它們相加後,得到的二進位結果轉換回十進位,就變成了:0.30000000000000004...

如何避免小數運算失真,與過大值的問題?

最簡單的方式,就是使用像是 decimal.js、big.js、BigNumber.js 這種運算函式庫來幫助我們,這些庫會用字串或陣列來模擬數學運算,避開 IEEE 754 的失真和誤差。

如果只是很簡單的運算,不想要直接引入一整個庫,我們也可以將小數放大為整數運算 (Scaling / Fixed-point):

// 錯誤做法
let total = 0.1 + 0.2; // 0.30000000000000004

// 放大為整數運算
let total = (0.1 * 10 + 0.2 * 10) / 10; // 0.3

或是使用 BigInt 處理超大整數,這是 ES2020 引入的新型別,專門用來處理任意長度的整數。它不會像 Number 一樣有精度限制,也沒有 MAX_VALUE 的上限,只要記憶體裝得下,它就能維持整數的精度。

let big = 9007199254740991n; // 後面加個 n 表示 BigInt
console.log(big + 2n); // 9007199254740993n (精確無誤)

結論

讀到這邊,相信你已經知道 Number 型別的有限了,尤其是在極大數和小數運算上會有風險,也因此我們一定在注意以下三種場景:

  1. 金流與財務系統
  2. 與後端對接大數字 ID
  3. 高精度科學運算或數據分析

當我們在處理這些場景時,一定要多檢查有沒有發生以上提到的問題!

那今天就這樣,下篇貼文見啦!

你可能會感興趣的文章 👇