0.1 + 0.2不等於0.3?為什麼JavaScript有這種“騷”操作?

Gladyu發表於2018-09-16

寫在前面

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

JavaScript中計算的翻車現場

言歸正傳 書接上文,先來一個簡單(landajie)的?,展示一下js計算的常規操作

0.1 + 0.2不等於0.3?為什麼JavaScript有這種“騷”操作?
這種送分題,js卻送了命。令人窒息的操作。這個例子很常見,我們不是為了關注這個例子本身,我們需要明白的是為什麼會出現這樣的結果?哪一步出了問題?還有那些計算可能會出現這樣的問題?怎麼解決?

JavaScript是如何表示數字的?

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

通過圖片具體看一下數字在記憶體中的表示

0.1 + 0.2不等於0.3?為什麼JavaScript有這種“騷”操作?
圖片文字說明

  • 第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位)

簡單驗證一下

0.1 + 0.2不等於0.3?為什麼JavaScript有這種“騷”操作?

運算時發生了什麼?

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

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

1.進位制轉換

0.1和0.2轉換成二進位制後會無限迴圈

0.1 -> 0.0001100110011001...(無限迴圈)
0.2 -> 0.0011001100110011...(無限迴圈)
複製程式碼

但是由於IEEE 754尾數位數限制,需要將後面多餘的位截掉(本文藉助這個網站直觀展示浮點數在記憶體中的二進位制表示)

0.1

0.1 + 0.2不等於0.3?為什麼JavaScript有這種“騷”操作?

0.2

0.1 + 0.2不等於0.3?為什麼JavaScript有這種“騷”操作?
這樣在進位制之間的轉換中精度已經損失

這裡還有一個小知識點

那為什麼 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不要去糾結哪種語言了,文章重點不是語言,謝謝

相關文章