前端進階
-JavaScript 64 位元浮點數最詳細的中文解釋
this.web
你知道嗎?電腦裡的數字和你看到的其實不一樣。
當你在 JavaScript 裡寫 0.1、3.14 或 1000時,電腦會用一種特殊的方式儲存起來,這種方式,就是 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 的指數:用來存這個
4(2⁴的 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 + 1 和 9007199254740991 + 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 位元尾數)是有限的,這導致了微小的捨入誤差。
我們可以把這個過程拆解為三個步驟:
- 十進位轉二進位 當我們在十進位寫下 0.1 時,這是一個簡短的數字。但在二進位中,許多十進位小數無法被精確表示。
- 十進位的 0.1:在二進位中會變成 0.0001100110011...(0011 無限循環)。
- 十進位的 0.2:在二進位中則是 0.001100110011...(0011 無限循環)。
- 52 位元的限制 因為 IEEE 754 64 位元格式只給了尾數 52 個位元的空間,電腦無法儲存無限循環的數字。
- 電腦必須在第 52 位元的地方進行四捨五入(或捨入到最接近的偶數)。
- 因此,存進記憶體的 0.1 其實並不是真正的 0.1,而是一個極其接近的近似值。
- 近似值相加的結果 當你執行
0.1 + 0.2時,電腦實際上是在做兩個「不精準的近似值」相加:- 電腦取出儲存的 0.1 近似值。
- 電腦取出儲存的 0.2 近似值。
- 將它們相加後,得到的二進位結果轉換回十進位,就變成了:
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 型別的有限了,尤其是在極大數和小數運算上會有風險,也因此我們一定在注意以下三種場景:
- 金流與財務系統
- 與後端對接大數字 ID
- 高精度科學運算或數據分析
當我們在處理這些場景時,一定要多檢查有沒有發生以上提到的問題!
那今天就這樣,下篇貼文見啦!