JavaScript 浮點數運算的精度問題

来源:https://www.cnblogs.com/digdeep/archive/2023/10/10/17753893.html
-Advertisement-
Play Games

本篇文章主要是使用 NestJs + Sequelize + MySQL 完成基礎運行, 帶大家瞭解 Node 服務端的基礎搭建,也可以順便看看 Java SpringBoot 項目的基礎結構,它倆真的非常相似,不信你去問服務端開發同學。 ...


來源:https://zhuanlan.zhihu.com/p/191395766

問題描述

在 JavaScript 中整數和浮點數都屬於 Number 數據類型,所有數字都是以 64 位浮點數形式儲存,即便整數也是如此。 所以我們在列印 1.00 這樣的浮點數的結果是 1 而非 1.00 。在一些特殊的數值表示中,例如金額,這樣看上去有點變扭,但是至少值是正確了。然而要命的是,當浮點數做數學運算的時候,你經常會發現一些問題,舉幾個例子:

JavaScript 代碼:

// 加法 =====================
// 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 不會?我們來看看其真正的原因。

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.10.2都會被轉換成二進位,但由於浮點數用二進位表達時是無窮的,例如。

JavaScript 代碼:

0.1 -> 0.0001100110011001...(無限)
0.2 -> 0.0011001100110011...(無限)

 

IEEE 754 標準的 64 位雙精度浮點數的小數部分最多支持 53 位二進位位,所以兩者相加之後得到二進位為:

JavaScript 代碼:

0.0100110011001100110011001100110011001100110011001100 

 

因浮點數小數位的限制而截斷的二進位數字,再轉換為十進位,就成了 0.30000000000000004。所以在進行算術計算時會產生誤差。

整數的精度問題

在 Javascript 中,整數精度同樣存在問題,先來看看問題:

JavaScript 代碼:

console.log(19571992547450991); //=> 19571992547450990
console.log(19571992547450991===19571992547450992); //=> true

 

同樣的原因,在 JavaScript 中 Number類型統一按浮點數處理,整數是按最大54位來算最大(253 - 1Number.MAX_SAFE_INTEGER,9007199254740991) 和最小(-(253 - 1)Number.MIN_SAFE_INTEGER,-9007199254740991) 安全整數範圍的。所以只要超過這個範圍,就會存在被捨去的精度問題。

當然這個問題並不只是在 Javascript 中才會出現,幾乎所有的編程語言都採用了 IEEE-745 浮點數表示法,任何使用二進位浮點數的編程語言都會有這個問題,只不過在很多其他語言中已經封裝好了方法來避免精度的問題,而 JavaScript 是一門弱類型的語言,從設計思想上就沒有對浮點數有個嚴格的數據類型,所以精度誤差的問題就顯得格外突出。

解決方案

上面說了這麼多問題和原因,這裡給出一些解決方案。

類庫

通常這種對精度要求高的計算都應該交給後端去計算和存儲,因為後端有成熟的庫來解決這種計算問題。前端也有幾個不錯的類庫:

Math.js

Math.js 是專門為 JavaScript 和 Node.js 提供的一個廣泛的數學庫。它具有靈活的表達式解析器,支持符號計算,配有大量內置函數和常量,並提供集成解決方案來處理不同的數據類型
像數字,大數字(超出安全數的數字),複數,分數,單位和矩陣。 功能強大,易於使用。

官網:

GitHub:

decimal.js

為 JavaScript 提供十進位類型的任意精度數值。

官網:

GitHub:

big.js

官網:

GitHub:

這幾個類庫幫我們解決很多這類問題,不過通常我們前端做這類運算通常只用於表現層,應用並不是很多。所以很多時候,一個函數能解決的問題不需要引用一個類庫來解決。

下麵介紹各個更加簡單的解決方案。

整數表示

對於整數,我們可以通過用String類型的表示來取值或傳值,否則會喪失精度。

格式化數字、金額、保留幾位小數等

如果只是格式化數字、金額、保留幾位小數等可以查看這裡 

浮點數運算

toFixed() 方法

浮點數運算的解決方案有很多,這裡給出一種目前常用的解決方案, 在判斷浮點數運算結果前對計算結果進行精度縮小,因為在精度縮小的過程總會自動四捨五入。

toFixed() 方法使用定點表示法來格式化一個數,會對結果進行四捨五入。語法為:

JavaScript 代碼:

numObj.toFixed(digits)

 

參數 digits 表示小數點後數字的個數;介於 0 到 20 (包括)之間,實現環境可能支持更大範圍。如果忽略該參數,則預設為 0。

返回一個數值的字元串表現形式,不使用指數記數法,而是在小數點後有 digits 位數字。該數值在必要時進行四捨五入,另外在必要時會用 0 來填充小數部分,以便小數部分有指定的位數。 如果數值大於 1e+21,該方法會簡單調用 Number.prototype.toString()並返回一個指數記數法格式的字元串。

特別註意:toFixed() 返回一個數值的字元串表現形式。

具體可以查看 MDN中的說明,那麼我們可以這樣解決精度問題:

JavaScript 代碼:

parseFloat((數學表達式).toFixed(digits)); // toFixed() 精度參數須在 0 與20 之間
// 運行
parseFloat((1.0 - 0.9).toFixed(10)) // 結果為 0.1   
parseFloat((0.3 / 0.1).toFixed(10)) // 結果為 3  
parseFloat((9.7 * 100).toFixed(10)) // 結果為 970 
parseFloat((2.22 + 0.1).toFixed(10)) // 結果為 2.32

 

在老版本的IE瀏覽器(IE 6,7,8)中,toFixed()方法返回值不一定准確。所以這個方法以前很少用。以至於網上搜索出來的結果大都是下麵這些方法。

還有一些其他的解決方案,簡單的說需要將浮點數轉換字元串,分隔成為整數部分和小數部分,小數部分再轉換為整數,計算結果後,再轉換為浮點數。這過程有點複雜…,網上找一下:

加法函數

JavaScript 代碼:

/**
 ** 加法函數,用來得到精確的加法結果
 ** 說明:javascript的加法結果會有誤差,在兩個浮點數相加的時候會比較明顯。這個函數返回較為精確的加法結果。
 ** 調用:accAdd(arg1,arg2)
 ** 返回值:arg1加上arg2的精確結果
 **/
function accAdd(arg1, arg2) {
 var r1, r2, m, c;
 try {
        r1 = arg1.toString().split(".")[1].length;
 }
 catch (e) {
        r1 = 0;
 }
 try {
        r2 = arg2.toString().split(".")[1].length;
 }
 catch (e) {
        r2 = 0;
 }
    c = Math.abs(r1 - r2);
    m = Math.pow(10, Math.max(r1, r2));
 if (c > 0) {
 var cm = Math.pow(10, c);
 if (r1 > r2) {
            arg1 = Number(arg1.toString().replace(".", ""));
            arg2 = Number(arg2.toString().replace(".", "")) * cm;
 } else {
            arg1 = Number(arg1.toString().replace(".", "")) * cm;
            arg2 = Number(arg2.toString().replace(".", ""));
 }
 } else {
        arg1 = Number(arg1.toString().replace(".", ""));
        arg2 = Number(arg2.toString().replace(".", ""));
 }
 return (arg1 + arg2) / m;
}
 
//給Number類型增加一個add方法,調用起來更加方便。
Number.prototype.add = function (arg) {
 return accAdd(arg, this);
};

 

減法函數

JavaScript 代碼:

/**
 ** 減法函數,用來得到精確的減法結果
 ** 說明:javascript的減法結果會有誤差,在兩個浮點數相減的時候會比較明顯。這個函數返回較為精確的減法結果。
 ** 調用:accSub(arg1,arg2)
 ** 返回值:arg1加上arg2的精確結果
 **/
function accSub(arg1, arg2) {
 var r1, r2, m, n;
 try {
        r1 = arg1.toString().split(".")[1].length;
 }
 catch (e) {
        r1 = 0;
 }
 try {
        r2 = arg2.toString().split(".")[1].length;
 }
 catch (e) {
        r2 = 0;
 }
    m = Math.pow(10, Math.max(r1, r2)); //last modify by deeka //動態控制精度長度
    n = (r1 >= r2) ? r1 : r2;
 return ((arg1 * m - arg2 * m) / m).toFixed(n);
}
 
// 給Number類型增加一個mul方法,調用起來更加方便。
Number.prototype.sub = function (arg) {
 return accMul(arg, this);
};

 

乘法函數

JavaScript 代碼:

/**
 ** 乘法函數,用來得到精確的乘法結果
 ** 說明:javascript的乘法結果會有誤差,在兩個浮點數相乘的時候會比較明顯。這個函數返回較為精確的乘法結果。
 ** 調用:accMul(arg1,arg2)
 ** 返回值:arg1乘以 arg2的精確結果
 **/
function accMul(arg1, arg2) {
 var m = 0, s1 = arg1.toString(), s2 = arg2.toString();
 try {
        m += s1.split(".")[1].length;
 }
 catch (e) {
 }
 try {
        m += s2.split(".")[1].length;
 }
 catch (e) {
 }
 return Number(s1.replace(".", "")) * Number(s2.replace(".", "")) / Math.pow(10, m);
}
 
// 給Number類型增加一個mul方法,調用起來更加方便。
Number.prototype.mul = function (arg) {
 return accMul(arg, this);
};

 

除法函數

JavaScript 代碼:

/** 
 ** 除法函數,用來得到精確的除法結果
 ** 說明:javascript的除法結果會有誤差,在兩個浮點數相除的時候會比較明顯。這個函數返回較為精確的除法結果。
 ** 調用:accDiv(arg1,arg2)
 ** 返回值:arg1除以arg2的精確結果
 **/
function accDiv(arg1, arg2) {
 var t1 = 0, t2 = 0, r1, r2;
 try {
        t1 = arg1.toString().split(".")[1].length;
 }
 catch (e) {
 }
 try {
        t2 = arg2.toString().split(".")[1].length;
 }
 catch (e) {
 }
 with (Math) {
        r1 = Number(arg1.toString().replace(".", ""));
        r2 = Number(arg2.toString().replace(".", ""));
 return (r1 / r2) * pow(10, t2 - t1);
 }
}
 
//給Number類型增加一個div方法,調用起來更加方便。
Number.prototype.div = function (arg) {
 return accDiv(this, arg);
};

 

相關推薦

1.JavaScript 精粹:Number 與 Math

2.javascript的基本數據類型有哪些

3.javascript教程


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文基於內核 5.4 版本源碼討論 通過上篇文章 《從內核世界透視 mmap 記憶體映射的本質(原理篇)》的介紹,我們現在已經非常清楚了 mmap 背後的映射原理以及它的使用方法,其核心就是在進程虛擬記憶體空間中分配一段虛擬記憶體出來,然後將這段虛擬記憶體與磁碟文件映射起來,整個 mmap 系統調用就結束了 ...
  • 2023_10_10_MYSQL_DAY_02_筆記 #在 FROM 子句中使用子查詢 SELECT a.ename, a.sal, a.deptno, b.salavg FROM emp a, (SELECT deptno, AVG(sal) salavg FROM emp GROUP BY de ...
  • 如今,大規模、高時效、智能化數據處理已是“剛需”,企業需要更強大的數據平臺,來應對數據查詢、數據處理、數據挖掘、數據展示以及多種計算模型並行的挑戰,湖倉一體方案應運而生。 《實時湖倉實踐五講》是袋鼠雲打造的系列直播活動,將圍繞實時湖倉的建設趨勢和通用問題,邀請奮戰於企業數字化一線的核心產品&技術專家 ...
  • PostgreSQL 教程 ​ — — — — —— 參考: 菜鳥教程 ORDBMS 術語 在我們開始學習 PostgreSQL 資料庫前,讓我們先瞭解下 ORDBMS 的一些術語: 資料庫: 資料庫是一些關聯表的集合。 數據表: 表是數據的矩陣。在一個資料庫中的表看起來像一個簡單的電子錶格。 列: ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 在ElementUI的世界中,不僅有基礎的組件和功能,還有一些讓你眼前一亮、*得不能再*的高級技巧和竅門。本文將揭示這些技巧,讓你在前端開發的舞臺上獨領風騷。無論你是一個勇敢的創新者還是一個喜歡調皮搗蛋的開發者,這些技巧都將讓你的Elem ...
  • 之前在開發一個管理頁面,功能有,編輯時只有一行可以編輯,刪除時彈出警告視窗,確認後才執行刪除。 ​ 代碼為Element-plus中的示例。 但是ElMessageBox一直被遮擋 代碼如下,均為Element-plus的示例,此外還有兩層router-view嵌套: <template> <el- ...
  • 在我們日常的網頁中,尤其是新聞類的網頁會遇到許多類似於這樣的樣式 多行甚至單行的文本隱藏+上省略號標題。 解決這一辦法,需要利用css的樣式進行改變 如下代碼的演示: 單行文本隱藏: 多行文本隱藏: 主要知識點: 1、單行文本隱藏省略:文本不能換行、超出部分隱藏、超出部分省略 2、多行文本隱藏省略: ...
  • 最近,在 Steam 玩一款老游戲(生化危機 4 重置版),其中,每當游戲轉場的過程中,都有這麼一個有趣的 Loading 動畫: 整個效果有點類似於日食效果,中間一圈黑色,向外散髮著太陽般的光芒。 本文,我們將嘗試使用 CSS,還原這個效果。 整個效果做出來,類似於如下兩個動畫效果這樣: 實現主體 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...