一. 前置知識點
1. 十進位制如何轉為二進位制?
整數部分除二取餘數, 直到商為0,逆序排列,小數部分乘2取整,順序排列,直到積中小數部分為0或者到達要求精度。
8轉為二進位制
8 / 2 = 4...0 取0
4 / 2 = 2...0 取0
2 / 2 = 1...0 取0
1 / 2 = 0...1 取1
二進位制結果為:1000
0.25轉為二進位制
0.25 * 2 = 0.50 取0
0.50 * 2 = 1.00 取1
二進位制結果為:01
於是可得出8.25的二進位制表示:1000.01
複製程式碼
2. 二進位制如何轉為十進位制?
注意:二進位制轉為十進位制不分整數部分與小數部分。
二進位制1000.01轉為十進位制
1 * 2^3 + 0 * 2^2 + 0 * 2^1 + 0 * 2^0 + 0 * 2^-1 + 0 * 2^-2 = 8.25
複製程式碼
二. javascript是如何儲存數字的
JavaScript
裡的數字是採用 IEEE 754 標準的 64 位 double 雙精度浮點數
-
sign bit(符號): 用來表示正負號,1位 (0表示正,1表示負)
-
exponent(指數): 用來表示次方數,11位
-
mantissa(尾數): 用來表示精確度,52位
對於沒有接觸的讀者來說,以上可能理解起來很模糊,沒關係,接下來我們用案例具體說明其流程,先看一下上述的十進位制數8.25在JS中是如何儲存的
- 十進位制的
8.25
會被轉化為二進位制的1000.01
; - 二進位制
1000.01
可用二進位制的科學計數法1.00001 * 2^4
表示; 1.00001 * 2^4
的小數部分00001
(二進位制)就是mantissa(尾數)了,4
(十進位制)加上1023
就是exponent(指數)了(這裡後面講解為什麼要加上1023);- 接下來指數
4
要加上1023
後轉為二進位制10000000011
; - 我們的十進位制
8.25
是一個正數,所以符號為二進位制表示為0
8.25
最終的二進位制儲存0-10000000011-0000100000000000000000000000000000000000000000000000
注意點:
- 不夠位的我們都用0補充;
- 步驟2得出的科學計數中的整數本分1我們好像忘記,這裡因為Javascript為了更最大限度的提高精確度,而省略了這個1,
這樣在我們我們本來只能儲存(二進位制)52位的尾數,實際是有(二進位制)53位的; - 指數部分是11位,表示的範圍是[0, 2047],由於科學計數中的指數可正可負,所以,中間數為 1023,[0,1022] 表示為負,[1024,2047] 表示為正,
這也解釋了為什麼我們科學計數中的指數要加上1023進行儲存了。
三. javascript是如何讀取數字的
我們還是以8.25的二進位制0-10000000011-0000100000000000000000000000000000000000000000000000
來講述
- 首先我們獲取指數部分的二進位制
1000000001
,轉化為十進位制為1027
,1027
減去1023
就是我們實際的指數4
了; - 獲取尾數部分
0000100000000000000000000000000000000000000000000000
實際是0.00001
(後面的0就不寫了),然後加上我們忽略的1
,得出1.00001
; - 因為首位為
0
,所以我們的數為正數,得出二進位制的科學計數為1.00001 * 2^4
,接著再轉為十進位制數,就得到了我們的8.25
;
四. 從0.1+0.2來看javascript精度問題
這裡就要進入我們的正題了,看懂了前面的原理說明,這部分將會變得很好理解了。
要計算0.1+0.2
,首先計算要先讀取到這兩個浮點數
0.1儲存為64位二進位制浮點數
沒有忘記以上步驟吧~
- 先將0.1轉化為二進位制的整數部分為
0
,小數部分為0001100110011001100110011001100110011...
咦,這裡居然進入了無限迴圈,那怎麼辦呢?暫時先不管; - 我們得到的無限迴圈的二進位制數用科學計數表示為
1.100110011001100110011001100110011... * 2^-4
; - 指數位即是
-4 + 1023 = 1019
,轉化位11位二進位制數01111111011
; - 尾數位是無限迴圈的,但是雙精度浮點數規定尾數位52位,於是超出52位的將被略去,保留
1001100110011001100110011001100110011001100110011010
- 最後得出0.1的64位二進位制浮點數:
0-01111111011-1001100110011001100110011001100110011001100110011010
同上,0.2儲存為64位二進位制浮點數:0-01111111100-1001100110011001100110011001100110011001100110011010
讀取到兩個浮點數的64為二進位制後,再將其轉化為可計算的二進位制數
- 0.1轉化為
1.1001100110011001100110011001100110011001100110011010 * 2^(1019 - 1023)
——0.00011001100110011001100110011001100110011001100110011010
; - 0.2轉化為
1.1001100110011001100110011001100110011001100110011010 * 2^(1020 - 1023)
——0.0011001100110011001100110011001100110011001100110011010
;
接著將兩個浮點數的二進位制數進行加法運算,得出0.0100110011001100110011001100110011001100110011001100111
轉化為十進位制數即為0.30000000000000004
不難看出,精度缺失是在儲存這一步就丟失了,後面的計算只是在不精準的值上進行的運算。
五. javascript如何解決精度問題出現的計算錯誤問題
對於小數或者整數的簡單運算可如下解決:
function numAdd(num1, num2) {
let baseNum, baseNum1, baseNum2;
try {
baseNum1 = String(num1).split(".")[1].length;
} catch (e) {
baseNum1 = 0;
}
try {
baseNum2 = String(num2).split(".")[1].length;
} catch (e) {
baseNum2 = 0;
}
baseNum = Math.pow(10, Math.max(baseNum1, baseNum2));
return (num1 * baseNum + num2 * baseNum) / baseNum;
};
複製程式碼
如:0.1 + 0.2
通過函式處理後,相當於 (0.1 * 10 + 0.2 * 10) / 10
但是如同我們前面所瞭解的,浮點數在儲存的時候就已經丟失精度了,所以浮點數乘以一個基數仍然會存在精度缺失問題,比如2500.01 * 100 = 250001.00000000003
,
所以我們可以在以上函式的結果之上使用toFixed(),保留需要的小數位數。
一些複雜的計算,可以引入一些庫進行解決。