JS計算精度小記

binnear發表於2019-03-01

一. 前置知識點

1. 十進位制如何轉為二進位制?

整數部分除二取餘數, 直到商為0,逆序排列,小數部分乘2取整,順序排列,直到積中小數部分為0或者到達要求精度

8轉為二進位制

8 / 2 = 4...00
4 / 2 = 2...00
2 / 2 = 1...00
1 / 2 = 0...11

二進位制結果為:1000

0.25轉為二進位制

0.25 * 2 = 0.500
0.50 * 2 = 1.001

二進位制結果為: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 雙精度浮點數

  1. sign bit(符號): 用來表示正負號,1位 (0表示正,1表示負)

  2. exponent(指數): 用來表示次方數,11位

  3. mantissa(尾數): 用來表示精確度,52位

JS計算精度小記

對於沒有接觸的讀者來說,以上可能理解起來很模糊,沒關係,接下來我們用案例具體說明其流程,先看一下上述的十進位制數8.25在JS中是如何儲存的

  1. 十進位制的8.25會被轉化為二進位制的1000.01
  2. 二進位制1000.01可用二進位制的科學計數法1.00001 * 2^4表示;
  3. 1.00001 * 2^4的小數部分00001(二進位制)就是mantissa(尾數)了,4(十進位制)加上1023就是exponent(指數)了(這裡後面講解為什麼要加上1023);
  4. 接下來指數4要加上1023後轉為二進位制10000000011
  5. 我們的十進位制8.25是一個正數,所以符號為二進位制表示為0
  6. 8.25最終的二進位制儲存0-10000000011-0000100000000000000000000000000000000000000000000000

注意點:

  1. 不夠位的我們都用0補充;
  2. 步驟2得出的科學計數中的整數本分1我們好像忘記,這裡因為Javascript為了更最大限度的提高精確度,而省略了這個1,
    這樣在我們我們本來只能儲存(二進位制)52位的尾數,實際是有(二進位制)53位的;
  3. 指數部分是11位,表示的範圍是[0, 2047],由於科學計數中的指數可正可負,所以,中間數為 1023,[0,1022] 表示為負,[1024,2047] 表示為正,
    這也解釋了為什麼我們科學計數中的指數要加上1023進行儲存了。

三. javascript是如何讀取數字的

我們還是以8.25的二進位制0-10000000011-0000100000000000000000000000000000000000000000000000來講述

  1. 首先我們獲取指數部分的二進位制1000000001,轉化為十進位制為10271027減去1023就是我們實際的指數4了;
  2. 獲取尾數部分0000100000000000000000000000000000000000000000000000實際是0.00001(後面的0就不寫了),然後加上我們忽略的1,得出1.00001
  3. 因為首位為0,所以我們的數為正數,得出二進位制的科學計數為1.00001 * 2^4,接著再轉為十進位制數,就得到了我們的8.25

四. 從0.1+0.2來看javascript精度問題

這裡就要進入我們的正題了,看懂了前面的原理說明,這部分將會變得很好理解了。

要計算0.1+0.2,首先計算要先讀取到這兩個浮點數

0.1儲存為64位二進位制浮點數

沒有忘記以上步驟吧~

  1. 先將0.1轉化為二進位制的整數部分為0,小數部分為0001100110011001100110011001100110011...咦,這裡居然進入了無限迴圈,那怎麼辦呢?暫時先不管;
  2. 我們得到的無限迴圈的二進位制數用科學計數表示為1.100110011001100110011001100110011... * 2^-4
  3. 指數位即是-4 + 1023 = 1019,轉化位11位二進位制數01111111011
  4. 尾數位是無限迴圈的,但是雙精度浮點數規定尾數位52位,於是超出52位的將被略去,保留1001100110011001100110011001100110011001100110011010
  5. 最後得出0.1的64位二進位制浮點數:0-01111111011-1001100110011001100110011001100110011001100110011010

同上,0.2儲存為64位二進位制浮點數:0-01111111100-1001100110011001100110011001100110011001100110011010

讀取到兩個浮點數的64為二進位制後,再將其轉化為可計算的二進位制數

  1. 0.1轉化為1.1001100110011001100110011001100110011001100110011010 * 2^(1019 - 1023)——0.00011001100110011001100110011001100110011001100110011010;
  2. 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(),保留需要的小數位數。

一些複雜的計算,可以引入一些庫進行解決。

相關文章