閱讀完本文可以瞭解到 0.1 + 0.2
為什麼等於 0.30000000000000004
以及 JavaScript 中最大安全數是如何來的。
十進位制小數轉為二進位制小數方法
拿 173.8125 舉例如何將之轉化為二進位制小數。
①. 針對整數部分 173,採取除 2 取餘,逆序排列
;
1 2 3 4 5 6 7 8 |
173 / 2 = 86 ... 1 86 / 2 = 43 ... 0 43 / 2 = 21 ... 1 ↑ 21 / 2 = 10 ... 1 | 逆序排列 10 / 2 = 5 ... 0 | 5 / 2 = 2 ... 1 | 2 / 2 = 1 ... 0 1 / 2 = 0 ... 1 |
得整數部分的二進位制為 10101101
。
②. 針對小數部分 0.8125,採用乘 2 取整,順序排列
;
1 2 3 4 |
0.8125 * 2 = 1.625 | 0.625 * 2 = 1.25 | 順序排列 0.25 * 2 = 0.5 | 0.5 * 2 = 1 ↓ |
得小數部分的二進位制為 1101
。
③. 將前面兩部的結果相加,結果為 10101101.1101
;
小心,二進位制小數丟失了精度!
根據上面的知識,將十進位制小數 0.1
轉為二進位制:
1 2 3 4 5 6 7 8 9 10 |
0.1 * 2 = 0.2 0.2 * 2 = 0.4 // 注意這裡 0.4 * 2 = 0.8 0.8 * 2 = 1.6 0.6 * 2 = 1.2 0.2 * 2 = 0.4 // 注意這裡,迴圈開始 0.4 * 2 = 0.8 0.8 * 2 = 1.6 0.6 * 2 = 1.2 ... |
可以發現有限十進位制小數 0.1
卻轉化成了無限二進位制小數 0.00011001100...
,可以看到精度在轉化過程中丟失了!
能被轉化為有限二進位制小數的十進位制小數的最後一位必然以 5 結尾(因為只有 0.5 * 2 才能變為整數)。所以十進位制中一位小數 0.1 ~ 0.9
當中除了 0.5
之外的值在轉化成二進位制的過程中都丟失了精度。
推導 0.1 + 0.2 為何等於 0.30000000000000004
在 JavaScript 中所有數值都以 IEEE-754 標準的 64 bit
雙精度浮點數進行儲存的。先來了解下 IEEE-754 標準下的雙精度浮點數。
這幅圖很關鍵,可以從圖中看到 IEEE-754 標準下雙精度浮點數由三部分組成,分別如下:
- sign(符號): 佔 1 bit, 表示正負;
- exponent(指數): 佔 11 bit,表示範圍;
- mantissa(尾數): 佔 52 bit,表示精度,多出的末尾如果是 1 需要進位;
推薦閱讀 JavaScript 浮點數陷阱及解法,閱讀完該文後可以瞭解到以下公式的由來。
精度位總共是 53 bit,因為用科學計數法表示,所以首位固定的 1 就沒有佔用空間。即公式中 (M + 1) 裡的 1。另外公式裡的 1023 是 2^11 的一半。小於 1023 的用來表示小數,大於 1023 的用來表示整數。
指數可以控制到 2^1024 – 1,而精度最大隻達到 2^53 – 1,兩者相比可以得出 JavaScript 實際可以精確表示的數字其實很少。
0.1
轉化為二進位制為 0.0001100110011...
,用科學計數法表示為 1.100110011... x 2^(-4)
,根據上述公式,S
為 0
(1 bit),E
為 -4 + 1023
,對應的二進位制為 01111111011
(11 bit),M
為 1001100110011001100110011001100110011001100110011010
(52 bit,另外注意末尾的進位),0.1
的儲存示意圖如下:
同理,0.2
轉化為二進位制為 0.001100110011...
,用科學計數法表示為 1.100110011... x 2^(-3)
,根據上述公式,E
為 -3 + 1023
,對應的二進位制為 01111111100
, M
為 1001100110011001100110011001100110011001100110011010
, 0.2
的儲存示意圖如下:
0.1 + 0.2
即 2^(-4) x 1.1001100110011001100110011001100110011001100110011010 與 2^(-3) x 1.1001100110011001100110011001100110011001100110011010 之和
1 2 3 4 5 6 |
// 計算過程 0.00011001100110011001100110011001100110011001100110011010 0.0011001100110011001100110011001100110011001100110011010 // 相加得 0.01001100110011001100110011001100110011001100110011001110 |
0.01001100110011001100110011001100110011001100110011001110
轉化為十進位制就是 0.30000000000000004
。驗證完成!
JavaScript 的最大安全數是如何來的
根據雙精度浮點數的構成,精度位數是 53 bit
。安全數的意思是在 -2^53 ~ 2^53
內的整數(不包括邊界)與唯一的雙精度浮點數互相對應。舉個例子比較好理解:
1 |
Math.pow(2, 53) === Math.pow(2, 53) + 1 // true |
Math.pow(2, 53)
竟然與 Math.pow(2, 53) + 1
相等!這是因為 Math.pow(2, 53) + 1 已經超過了尾數的精度限制(53 bit),在這個例子中 Math.pow(2, 53)
和 Math.pow(2, 53) + 1
對應了同一個雙精度浮點數。所以 Math.pow(2, 53)
就不是安全數了。
最大的安全數為
Math.pow(2, 53) - 1
,即9007199254740991
。
業務中碰到的精度問題以及解決方案
瞭解 JavaScript 精度問題對我們業務有什麼幫助呢?舉個業務場景:比如有個訂單號後端 Java 同學定義的是 long 型別,但是當這個訂單號轉換成 JavaScript 的 Number 型別時候精度會丟失了,那沒有以上知識鋪墊那就理解不了精度為什麼會丟失。
解決方案大致有以下幾種:
1.針對大數的整數可以考慮使用 bigint 型別(目前在 stage 3 階段);
2.使用 bigNumber,它的思想是轉化成 string 進行處理,這種方式對效能有一定影響;
3.可以考慮使用 long.js,它的思想是將 long 型別的值轉化成兩個 32 位的雙精度型別的值。
4.針對小數可以考慮 JavaScript 浮點數陷阱及解法 裡面提到的方案;