0.1 + 0.2不等於0.3?爲何JavaScript有這種「騷」操做?

寫在前面

隨着消費觀念的改變,線上消費已經成爲大衆生活中不可或缺的一部分。在保證消費安全和用戶隱私的同時,精準度也是必不可少的一環。試想一下,用戶在一款產品上消費,結算金額出錯,用戶會怎麼想?(數體教 or WTF?),妥妥的差評了吧。 這樣不要說用戶粘性了,留存都是問題。當Boss得知用戶的遭遇後,估計貢獻代碼的同志會成爲前員工或者你們口中的已故員工某某某。做爲一個優秀(laji)的程序員,很久以前就遇到過精確計算的問題,可是偷懶並無整理出來,直到最近有人問我相關問題,忽然以爲有必要寫寫我對js精確計算的理解html

JavaScript中計算的翻車現場

言歸正傳 書接上文,先來一個簡單(landajie)的🌰,展現一下js計算的常規操做 git

這種送分題,js卻送了命。使人窒息的操做。這個例子很常見,咱們不是爲了關注這個例子自己,咱們須要明白的是 爲何會出現這樣的結果?哪一步出了問題?還有那些計算可能會出現這樣的問題?怎麼解決?

JavaScript是如何表示數字的?

JavaScript使用Number類型表示數字(整數和浮點數),遵循 IEEE 754 標準 經過64位來表示一個數字程序員

經過圖片具體看一下數字在內存中的表示github

圖片文字說明

  • 第0位:符號位,0表示正數,1表示負數(s)
  • 第1位到第11位:儲存指數部分(e)
  • 第12位到第63位:儲存小數部分(即有效數字)f

既然說到這裏,再給你們科普一個小知識點:js最大安全數是 Number.MAX_SAFE_INTEGER == Math.pow(2,53) - 1, 而不是Math.pow(2,52) - 1, why?尾數部分不是隻有52位嗎?安全

這是由於二進制表示有效數字老是1.xx…xx的形式,尾數部分f在規約形式下第一位默認爲1(省略不寫,xx..xx爲尾數部分f,最長52位)。所以,JavaScript提供的有效數字最長爲53個二進制位(64位浮點的後52位+被省略的1位)bash

簡單驗證一下 函數

運算時發生了什麼?

首先,計算機沒法直接對十進制的數字進行運算,這是硬件物理特性已經決定的。這樣運算就分紅了兩個部分:先按照IEEE 754轉成相應的二進制,而後對階運算網站

按照這個思路分析一下0.1 + 0.2的運算過程ui

1.進制轉換

0.1和0.2轉換成二進制後會無限循環spa

0.1 -> 0.0001100110011001...(無限循環)
0.2 -> 0.0011001100110011...(無限循環)
複製代碼

可是因爲IEEE 754尾數位數限制,須要將後面多餘的位截掉(本文藉助這個網站直觀展現浮點數在內存中的二進制表示)

0.1

0.2

這樣在進制之間的轉換中精度已經損失

這裏還有一個小知識點

那爲何 x=0.1 能獲得 0.1?

這是由於這個 0.1 並非真正的0.1。這不是廢話嗎?別急,聽我解釋

標準中規定尾數f的固定長度是52位,再加上省略的一位,這53位是JS精度範圍。它最大能夠表示2^53(9007199254740992), 長度是 16,因此可使用 toPrecision(16) 來作精度運算,超過的精度會自動作湊整處理

0.10000000000000000555.toPrecision(16)
// 返回 0.1000000000000000,去掉末尾的零後正好爲 0.1

// 但來一個更高的精度:
0.1.toPrecision(21) = 0.100000000000000005551
複製代碼

這個就是爲何0.1能夠等於0.1的緣由。好的,繼續

2.對階運算

因爲指數位數不相同,運算時須要對階運算 這部分也可能產生精度損失

按照上面兩步運算(包括兩步的精度損失),最後的結果是

0.0100110011001100110011001100110011001100110011001100 
複製代碼

結果轉換成十進制以後就是0.30000000000000004,這樣就有了前面的「秀」操做:0.1 + 0.2 != 0.3

因此:

精度損失可能出如今進制轉化和對階運算過程當中

精度損失可能出如今進制轉化和對階運算過程當中

精度損失可能出如今進制轉化和對階運算過程當中

只要在這兩步中產生了精度損失,計算結果就會出現誤差

怎麼解決精度問題?

1.將數字轉成整數

這是最容易想到的方法,也相對簡單

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;
}
複製代碼

可是這種方法對大數支持的依然很差

2.三方庫

這個是比較全面的作法,推薦2個我平時接觸到的庫

1).Math.js

專門爲 JavaScript 和 Node.js 提供的一個普遍的數學庫。支持數字,大數字(超出安全數的數字),複數,分數,單位和矩陣。 功能強大,易於使用。

官網:mathjs.org/

GitHub:github.com/josdejong/m…

2).big.js

官網:mikemcl.github.io/big.js

GitHub:github.com/MikeMcl/big…

3)若干,不一一列舉了

這幾個類庫都很牛逼,能夠應對各類各樣的需求,不過不少時候,一個函數能解決的問題不須要引用一個類庫來解決。

以上就是我對js精準計算的理解,但願能夠幫到你們

轉載必須標明出處,謝謝。文章有疏漏淺薄之處,請各位大神斧正

說明

看了評論不少人說:其餘遵循 IEEE 754 標準的語言也有這個問題,我知道其餘的語言也有,可是這篇文章是以js爲切入點去分析的,so不要去糾結哪一種語言了,文章重點不是語言,謝謝

看了評論不少人說:其餘遵循 IEEE 754 標準的語言也有這個問題,我知道其餘的語言也有,可是這篇文章是以js爲切入點去分析的,so不要去糾結哪一種語言了,文章重點不是語言,謝謝

看了評論不少人說:其餘遵循 IEEE 754 標準的語言也有這個問題,我知道其餘的語言也有,可是這篇文章是以js爲切入點去分析的,so不要去糾結哪一種語言了,文章重點不是語言,謝謝

相關文章
相關標籤/搜索