這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助
前言
前端開發中難免會遇到價格和金額計算的需求,這類需求所要計算的數值大多數情況下是要求精確到小數點後的多少位。但是因為JS語言本身的缺陷,在處理浮點數的運算時會出現一些奇怪的問題,導致計算不精確。
本文嘗試從現象入手,分析造成這一問題原因,並總結和整合一些通用的解決方案,以供大家參考。
現象回顧
下面的是JS進行數值運算過程中常見的問題,這個問題有個專業的名稱叫精度丟失。
在 JavaScript 中整數和浮點數都屬於 Number 資料型別,所有的數字都是以 64 位浮點數形式儲存,整數也是如此。所以我們在列印 1.00 這樣的浮點數的結果是 1 而非 1.00 。在一些特殊的數值表示中,例如金額,這樣看上去有點彆扭,但是至少值是正確了。然而要命的是,當浮點數做數學運算的時候,你經常會發現一些問題,舉幾個例子:
// 加法 0.1 + 0.2 = 0.30000000000000004 0.7 + 0.1 = 0.7999999999999999 0.2 + 0.4 = 0.6000000000000001 2.22 + 0.1 = 2.3200000000000003 // 減法 1.5 - 1.2 = 0.30000000000000004 0.3 - 0.2 = 0.09999999999999998 // 乘法 19.9 * 100 = 1989.9999999999998 19.9 * 10 * 10 = 1990 1306377.64 * 100 = 130637763.99999999 1306377.64 * 10 * 10 = 130637763.99999999 0.7 * 180 = 125.99999999999999 9.7 * 100 = 969.9999999999999 39.7 * 100 = 3970.0000000000005 // 除法 0.3 / 0.1 = 2.9999999999999996 0.69 / 10 = 0.06899999999999999
問題分析
JavaScript 裡的數字是採用 IEEE 754 標準的 64 位雙精度浮點數。
該規範定義了浮點數的格式,對於 64 位的浮點數在記憶體中的表示,最高的 1 位是符號位,接著的 11 位是指數,剩下的 52 位為有效數字,具體對應如下:
- 第 0 位:符號位, s 表示 ,0 表示正數,1 表示負數;
- 第 1 位到第 11 位:儲存指數部分, e 表示 ;
- 第 12 位到第 63 位:儲存小數部分(即有效數字),f 表示,
符號位決定了一個數的正負,指數部分決定了數值的大小,小數部分決定了數值的精度。
IEEE 754 規定,有效數字第一位預設總是 1,不儲存在 64 位浮點數之中。也就是說,有效數字總是 1.xx…xx 的形式,其中 xx..xx 的部分儲存在 64 位浮點數之中,最長可能為 52 位。
因此,JavaScript 提供的有效數字最長為 53 個二進位制位(64 位浮點的後 52 位 + 有效數字第一位的 1)。
那麼,JavaScript 在計算 0.1 + 0.2
時,到底發生了什麼呢?
首先,十進位制的 0.1
和 0.2
都會被轉換成二進位制,但由於浮點數用二進位制表達時是無窮的,就成了下面的樣子。
0.1 -> 0.0001100110011001...(無限) 0.2 -> 0.0011001100110011...(無限)
0.0100110011001100110011001100110011001100110011001100
最終,因浮點數小數位的限制而截斷的二進位制數字,再轉換為十進位制,就成了 0.30000000000000004
,所以在進行算術計算時就產生了誤差。
解決方案
通常情況下對計算精度要求高的業務場景都應該交給後端去計算和儲存,因為後端有成熟的方案和工具來解決這種計算問題。
這裡是對網上常見的幾個解決方案的彙總和整理,但是方案一和方案二都存在一定的侷限性,我會在對應的方案裡進行說明。通常我們前端做這類運算通常只用於表現層,所以這兩個方案基本是夠用的。
方案一
在應對確定精度浮點數運算時,可以把金額轉換成整數進行運算,最後再將結果轉換成真實金額。
// 定義金額(保留兩位小數) const amount1 = 0.10; const amount2 = 0.20; // 轉整數運算,並還原成真實金額 const result = ((amount1 * 100) + (amount2 * 100)) / 100; // 結果 console.log(result); // 0.3 // 直接運算 console.log(amount1 + amount2); // 0.30000000000000004
需要注意的是上面的例子只能處理最多兩位小數的運算場景,如果小數位不確定這個方法是行不通的。
方案二
JavaScript 內建的 toFixed() 方法可以將數字轉換成保留指定小數位的字串。這個方法適用於簡單的金額計算。但需要注意舍入誤差,因為轉換後是字串,失去了浮點數的特性,最後的結果坑你存在微小的誤差。
// 定義金額 const amount1 = 0.1; const amount2 = 0.2; // 加法運算 const result = (amount1 + amount2).toFixed(2); // 保留兩位小數 // 注意這裡運算的結果應該是:0.30000000000000004 console.log(result); // 輸出 "0.30"
toFixed 它是一個四捨六入五成雙的詭異的方法(也叫銀行家演算法)。"四捨六入五成雙"含義:對於位數很多的近似數,當有效位數確定後,其後面多餘的數字應該捨去,只保留有效數字最末一位,這種修約(舍入)規則是“四捨六入五成雙”,也即“4舍6入5湊偶”這裡“四”是指≤4 時捨去,"六"是指≥6時進上,"五"指的是根據5後面的數字來定,當5後有數時,舍5入1;當5後無有效數字時,需要分兩種情況來講:5前為奇數,舍5入1;5前為偶數,舍5不進(0是偶數)。
第三方庫
現代前端發展至今,已經有很多成熟的類庫來幫助我們解決此類問題,這類類庫通常有很好的通用性和相容性。
下面我將推薦幾個人氣較高的數字計算類庫。
Math.js
Math.js 是專門為 JavaScript 和 Node.js 提供的一個廣泛的數學庫。它具有靈活的表示式解析器,支援符號計算,配有大量內建函式和常量,並提供整合解決方案來處理不同的資料型別像數字,大數字 (超出安全數的數字),複數,分數,單位和矩陣,功能強大,易於使用。
官網:mathjs.org/
GitHub:GitHub - josdejong/mathjs: An extensive math library for JavaScript and Node.js
decimal.js
為 JavaScript 提供十進位制型別的任意精度數值。
官網:decimal.js API
GitHub:GitHub - MikeMcl/decimal.js: An arbitrary-precision Decimal type for JavaScript
big.js
Big.js 是一個用於處理任意精度的大數運算的 JavaScript 庫。它解決了 JavaScript 中處理大數運算時精度丟失的問題,提供了更高精度的計算能力。
Big.js 庫的特點包括:
- 任意精度:Big.js 允許您處理任意精度的數字,而不受 JavaScript 內建數字型別的限制。
- 高精度計算:Big.js 提供了精確的加法、減法、乘法、除法和取餘等運算,以及比較和舍入等功能。
- 可配置的精度和舍入規則:您可以自定義 Big.js 運算的精度和舍入規則,以滿足特定的需求。
- 支援鏈式操作:您可以使用鏈式呼叫來執行多個運算,使程式碼更簡潔易讀。
- 適用於瀏覽器和 Node.js:Big.js 可以在瀏覽器和 Node.js 環境中使用,相容性良好。
Big.js 庫非常適用於需要高精度計算的場景,如金融、密碼學、科學計算和大資料處理等。它允許開發人員在 JavaScript 中進行準確的數字計算,避免了精度損失帶來的問題。
官網:big.js API
GitHub:GitHub - MikeMcl/big.js: A small, fast JavaScript library for arbitrary-precision decimal arithmetic.
總結
本文對 Javascript 中浮點數運算出現的精度丟失問題進行了還原,分析了問題產生的原因在於二進位制本身。同時給出了三個網路上比較成熟的解決方案,其中第一和第二方案基本可以滿足大部分開發場景,如果不能滿足就使用類庫。
本文轉載於:
https://juejin.cn/post/7325627704782307337
如果對您有所幫助,歡迎您點個關注,我會定時更新技術文件,大家一起討論學習,一起進步。