JS數字精度


0.前言

最近在看計算機組成原理的浮點數部分,突然想起之前看過的一道快手面試題

為什么js中0.1+0.2不等於0.3,應該如何解決?

這里我們可以借這道題來說一下js的精度問題

 

1.JS數的儲存

二進制和浮點數和定點數

首先計算機里面的數據肯定以二進制形式存儲
對於同一段二進制碼,不同的解讀方式肯定有不同的意義
對於小數,我們有定點數和浮點數兩種表示方法
目前計算機大多用浮點數,精度高,表示范圍大

一個數以浮點數二進制碼形式儲存,我們從二進制浮點數碼中能算出表達的二進制,然后二進制又可以得到相應的十進制,這就是他們的轉化關系

復習浮點數

我們復習一下計組中對浮點數的介紹 這里以32位為例

如上圖,32位二進制碼中有三個部分,符號位,指數位,尾數位
浮點數計算公式:

從左往右看有三個部分,符號位指數位,尾數位
(1)(-1)^s表示符號位,當s=0,V為正數;當s=1,V為負數。
(2)2^E表示指數位。
(3)M表示有效數字,大於等於1,小於2。
這里有兩個注意點,
1.M由於恆定為1.xxx,所以默認省略1
2.E不全為0或不全為1。這時E=E-127或者E=E-1023(64位)

我們按上面的規則算一下為什么圖中0011111000100...00表示的0.15625
首先指數位置01111100表示的是124 則E=124-127=-3
然后我們看尾數,尾數位為1.01(加上了隱藏的1)
所以v=1.01*2的-3次方=0.00101
注意這個是二進制結果,轉化為十進制的就是2(-3次方)+2(-5次方)=0.125+0.125*0.25=0.15625

JS中數值

JavaScript 內部,所有數字都是以64位浮點數形式儲存,即使整數也是如此。所以,1與1.0是相同的,是同一個數。

我們看一下64位的JS數字是怎么儲存的

JS中1和0.1的表示方法

在http://www.binaryconvert.com/這個網站上我們可以找到一個數的二進制浮點數表示
我們手動按上面的方法算一下,並且驗證看對不對

1的表示方法

1對應的二進制1.00000
那么1對應的浮點數應該長成這樣
1.0* 2的0次方
指數為0 尾數為1.0 所以指數實際是1023 尾數是0000000000000...00

看來我們算的是對的

0.1的表示方法

0.1對應的二進制 0.000110011001100...(循環)
那么1對應的浮點數應該長成這樣
1.10011001100....*2的-4次方
所以尾數位應該是10011001100....
指數應該是1019也就是01111111011

看來我們是對的

 

2.精度產生的原因

為什么精度會產生呢

首先0.1這種轉化為二進制碼是有誤差的,尾數是一個不斷循環的數,明顯會有誤差

其次,在浮點數加法運算里面,有對階操作。対階會損失一部分尾數,如果尾數后面都是0,沒影響。但是如果是0.1轉換成的二進制浮點數碼的尾數,対階的時候舍棄部分尾數明顯也會造成誤差。

如果一個大數和一個小數相加時,會產生很大的誤差,因為対階的時候尾數得截掉好多位

(1+0.1).toPrecision(20) //"1.1000000000000000888" (100000000000+0.1).toPrecision(20) "100000000000.10000610"

很明顯誤差大了

東莞vi設計https://www.houdianzi.com/dgvi/ 豌豆資源網站大全https://55wd.com

3.關於精度的額外知識點

0.1並不是0.1

上面的內容你如果理解了,你再看到0.1,你就會清楚,0.1並不是0.1,0.1的浮點數二進制碼是有誤差的,不可能算出0.1
我們看到的是瀏覽器幫我們做了處理的

不信,你可以試試

0.1.toPrecision(17) // "0.10000000000000001"

JS安全數

面試喜歡考這個問題,啥叫安全數,就是在這個范圍數值都有一正一反,一一對應
尾數一共52位,加上一個隱藏位,共53位
也就是JS能表示的最大整數是2的53次方,這個數是16位
但是

Math.pow(2, 53) === Math.pow(2, 53) + // true

實際上2的53次方都不安全了,所以是2的53次方-1
在es6中 是Number.MAX_SAFE_INTEGER

toPrecision vs toFixed

數據處理時,這兩個函數很容易混淆。它們的共同點是把數字轉成字符串供展示使用。注意在計算的中間過程不要使用,只用於最終結果。

不同點就需要注意一下:

  • toPrecision是處理精度,精度是從左至右第一個不為0的數開始數起。
  • toFixed是小數點后指定位數取整,從小數點開始數起。

兩者都能對多余數字做湊整處理,也有些人用toFixed來做四舍五入,但一定要知道它是有 Bug 的。

如:1.005.toFixed(2)返回的是1.00而不是1.01。

原因:1.005實際對應的數字是1.00499999999999989,在四舍五入時全部被舍去!

怎么解決這個問題呢,引入mathjs用它的round方法也可以,自己寫一套字符串邏輯去處理也可以

 

4.解決方案

誤差主要產生在進制轉化和浮點數運算的対階操作中
整數由於尾數后面全是0,同時轉化為二進制數沒有誤差,所以

我們第一種方案就是全部轉化為整數,計算完再轉化為小數類似這種

/** * 精確加法 */ function add(num1, num2) { const num1Digits = (num1.toString().split('.')[1] || '').length; const num2Digits = (num2.toString().split('.')[1] || '').length; const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits)); return (num1 * baseNum + num2 * baseNum) / baseNum; } 

我們第二種方案就是用現成的庫 mathjs之類的,原理就是不走浮點數那一套,轉化成字符串,自己實現運算邏輯,從性能上說肯定比原生慢一點

當然如果僅僅是展示類型的,3.0000000001保留兩位小數的這種還是tofixed(2)最快哦


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM