最近在看計算機組成原理的浮點數部分,忽然想起以前看過的一道快手面試題javascript
爲何JS中0.1+0.2不等於0.3,應該如何解決?
這裏咱們能夠借這道題來講一下JS的精度問題html
首先計算機裏面的數據確定以二進制形式存儲
對於同一段二進制碼,不一樣的解讀方式確定有不一樣的意義
對於小數,咱們有定點數和浮點數兩種表示方法
目前計算機大多用浮點數,精度高,表示範圍大java
一個數以浮點數二進制碼形式儲存,咱們從二進制浮點數碼中能算出表達的二進制,而後二進制又能夠獲得相應的十進制,這就是他們的轉化關係git
咱們複習一下計組中對浮點數的介紹 這裏以32位爲例es6
如上圖,32位二進制碼中有三個部分,符號位,指數位,尾數位
浮點數計算公式:github
從左往右看有三個部分,符號位指數位,尾數位
(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瀏覽器
JavaScript 內部,全部數字都是以64位浮點數形式儲存,即便整數也是如此。因此,1
與1.0
是相同的,是同一個數。
咱們看一下64位的JS數字是怎麼儲存的安全
在 http://www.binaryconvert.com/... 這個網站上咱們能夠找到一個數的二進制浮點數表示
咱們手動按上面的方法算一下,而且驗證看對不對函數
1對應的二進制1.00000
那麼1對應的浮點數應該長成這樣
1.0* 2的0次方
指數爲0 尾數爲1.0 因此指數實際是1023 尾數是0000000000000...00
看來咱們算的是對的
0.1對應的二進制 0.000110011001100...(循環)
那麼1對應的浮點數應該長成這樣
1.10011001100....*2的-4次方
因此尾數位應該是10011001100....
指數應該是1019也就是01111111011
看來咱們是對的
爲何精度會產生呢
首先0.1這種轉化爲二進制碼是有偏差的,尾數是一個不斷循環的數,明顯會有偏差
其次,在浮點數加法運算裏面,有對階操做。対階會損失一部分尾數,若是尾數後面都是0,沒影響。可是若是是0.1轉換成的二進制浮點數碼的尾數,対階的時候捨棄部分尾數明顯也會形成偏差。
若是一個大數和一個小數相加時,會產生很大的偏差,由於対階的時候尾數得截掉好多位
(1+0.1).toPrecision(20) //"1.1000000000000000888" (100000000000+0.1).toPrecision(20) "100000000000.10000610"
很明顯偏差大了
上面的內容你若是理解了,你再看到0.1,你就會清楚,0.1並非0.1,0.1的浮點數二進制碼是有偏差的,不可能算出0.1
咱們看到的是瀏覽器幫咱們作了處理的
不信,你能夠試試
0.1.toPrecision(17) // "0.10000000000000001"
面試喜歡考這個問題,啥叫安全數,就是在這個範圍數值都有一正一反,一一對應
尾數一共52位,加上一個隱藏位,共53位
也就是JS能表示的最大整數是2的53次方,這個數是16位
可是
Math.pow(2, 53) === Math.pow(2, 53) + 1 // true
實際上2的53次方都不安全了,因此是2的53次方-1
在es6中 是Number.MAX_SAFE_INTEGER
數據處理時,這兩個函數很容易混淆。它們的共同點是把數字轉成字符串供展現使用。注意在計算的中間過程不要使用,只用於最終結果。
不一樣點就須要注意一下:
toPrecision
是處理精度,精度是從左至右第一個不爲0的數開始數起。toFixed
是小數點後指定位數取整,從小數點開始數起。二者都能對多餘數字作湊整處理,也有些人用toFixed
來作四捨五入,但必定要知道它是有 Bug 的。
如:1.005.toFixed(2)
返回的是1.00
而不是1.01
。
緣由:1.005
實際對應的數字是1.00499999999999989
,在四捨五入時所有被捨去!
怎麼解決這個問題呢,引入mathjs用它的round方法也能夠,本身寫一套字符串邏輯去處理也能夠
偏差主要產生在進制轉化和浮點數運算的対階操做中
整數因爲尾數後面全是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)最快哦