JavaScript計(jì)算出現(xiàn)精度丟失問(wèn)題的解決方法
前言
Javascript作為一門大型編程語(yǔ)言,在日常開發(fā)中難免會(huì)涉及到大量的數(shù)學(xué)計(jì)算。然而,浮點(diǎn)數(shù)在計(jì)算過(guò)程中可能出現(xiàn)精度的問(wèn)題,因此Javascript提供了一個(gè)高精度計(jì)算庫(kù)來(lái)幫助處理復(fù)雜的數(shù)字計(jì)算。本文就來(lái)介紹一下Javascript高精度計(jì)算及其相關(guān)知識(shí)。
首先,我們來(lái)看一個(gè)簡(jiǎn)單的例子:
0.1 + 0.2
結(jié)果不是 0.3,而是 0.30000000000000004
可以看到數(shù)字的精度已經(jīng)丟失,雖然結(jié)果相差無(wú)幾,但是作為技術(shù)人員,這絕對(duì)不可以忽略。 簡(jiǎn)單一句話概括解釋為什么你會(huì)得到意想不到的結(jié)果:
因?yàn)樵谟?jì)算機(jī)內(nèi)部,使用的二進(jìn)制浮點(diǎn)根本就不能準(zhǔn)確地表示像 0.1, 0.2 或 0.3 這樣的數(shù)字。
當(dāng)編碼或解釋代碼時(shí),你的 “0.1” 其實(shí)已經(jīng)舍入為和該數(shù)字的最接近的數(shù)字,即使在計(jì)算發(fā)生之前已經(jīng)會(huì)導(dǎo)致小的舍入誤差。
JavaScript 中的數(shù)字都是浮點(diǎn)數(shù),即使看起來(lái)像整數(shù)的數(shù)字也是。這是因?yàn)?JavaScript 使用 IEEE 754 標(biāo)準(zhǔn)來(lái)表示數(shù)字,這種表示方法對(duì)于大多數(shù)情況是足夠的,但在某些情況下可能導(dǎo)致精度丟失。
在涉及貨幣或其他需要精確計(jì)算的場(chǎng)景中,由于 JavaScript 浮點(diǎn)數(shù)的特性可能導(dǎo)致精度丟失,因此一種常見(jiàn)而有效的解決方案是將數(shù)字轉(zhuǎn)換為整數(shù)進(jìn)行計(jì)算,然后再將結(jié)果轉(zhuǎn)換回浮點(diǎn)數(shù)。這種做法能夠在一定程度上規(guī)避浮點(diǎn)數(shù)運(yùn)算中可能出現(xiàn)的舍入誤差,尤其在處理金融數(shù)據(jù)等對(duì)精確性要求極高的情況下顯得尤為重要。
let num1 = 0.1 * 10; // 轉(zhuǎn)換成整數(shù)進(jìn)行計(jì)算 let num2 = 0.2 * 10; let sum = (num1 + num2) / 10; // 轉(zhuǎn)換回浮點(diǎn)數(shù) console.log(sum); // 輸出:0.3
通過(guò)上面這種方式,我們可以在保留所需精度的同時(shí),規(guī)避掉 JavaScript 浮點(diǎn)數(shù)運(yùn)算可能引發(fā)的不精確性問(wèn)題。
但是也會(huì)出現(xiàn)其他問(wèn)題,增加小數(shù)點(diǎn)后面的位數(shù),會(huì)出現(xiàn)下面的情況:
20.24*100
// 2023.9999999999998
我們知道浮點(diǎn)型數(shù)據(jù)類型主要有:?jiǎn)尉萬(wàn)loat、雙精度double。
但是?。?!
JavaScript 存儲(chǔ)小數(shù)和其它語(yǔ)言如 Java 和 Python 都不同,JavaScript 中所有數(shù)字包括整數(shù)和小數(shù)都只有一種類型 即 Number類型 它的實(shí)現(xiàn)遵循 IEEE 754 標(biāo)準(zhǔn),IEEE 754 標(biāo)準(zhǔn)的內(nèi)容都有什么,這個(gè)咱不用管,我們只需要記住以下一點(diǎn):
javascript以64位雙精度浮點(diǎn)數(shù)存儲(chǔ)所有Number類型值,即計(jì)算機(jī)最多存儲(chǔ)64位二進(jìn)制數(shù)。
對(duì)于double型數(shù)據(jù)(雙精度浮點(diǎn)數(shù)),其長(zhǎng)度是8個(gè)字節(jié)(大小),右邊52位用來(lái)表示小數(shù)點(diǎn)后面的數(shù)字,中間11位表示e(exponent)小數(shù)點(diǎn)移動(dòng)的位數(shù),左邊一位用來(lái)表示正負(fù)。如圖所示:
解決方法
Number(parseFloat(20.24*100).toPrecision(16))
存儲(chǔ)二進(jìn)制時(shí)小數(shù)點(diǎn)的偏移量最大為52位,最多可表示的十進(jìn)制為9007199254740992,對(duì)應(yīng)科學(xué)計(jì)數(shù)尾數(shù)是 9.007199254740992,這也是 JavaScript 最多能表示的精度。它的長(zhǎng)度是 16,所以可以使用 toPrecision(16) 來(lái)做精度運(yùn)算。
通過(guò)先轉(zhuǎn)為浮點(diǎn)型計(jì)算,然后做精度運(yùn)算后再轉(zhuǎn)為Number類型即可。
但是不能保證還會(huì)不會(huì)有其他問(wèn)題,并且這樣的計(jì)算太繁瑣,每次都需要對(duì)數(shù)字進(jìn)行相應(yīng)的處理。
解決方案
我們將處理的計(jì)算問(wèn)題進(jìn)行統(tǒng)一封裝,可以專門處理精度問(wèn)題。代碼如下:
export class Calc{ /** * 加法運(yùn)算 * @param {number} num1 * @param {number} num2 * @returns {*} */ add(num1: number, num2: number): number { num1 = Number(num1); num2 = Number(num2); let dec1: number, dec2: number, times: number; try { dec1 = this.countDecimals(num1)+1; } catch (e) { dec1 = 0; } try { dec2 = this.countDecimals(num2)+1; } catch (e) { dec2 = 0; } times = Math.pow(10, Math.max(dec1, dec2)); const result = (this.mul(num1, times) + this.mul(num2, times)) / times; return this.getCorrectResult("add", num1, num2, result); } /** * 減法運(yùn)算 * @param {number} num1 * @param {number} num2 * @returns {number} */ sub(num1: number, num2: number): number { num1 = Number(num1); num2 = Number(num2); let dec1: number, dec2: number, times: number; try { dec1 = this.countDecimals(num1)+1; } catch (e) { dec1 = 0; } try { dec2 = this.countDecimals(num2)+1; } catch (e) { dec2 = 0; } times = Math.pow(10, Math.max(dec1, dec2)); const result = Number((this.mul(num1, times) - this.mul(num2, times)) / times); return this.getCorrectResult("sub", num1, num2, result); } /** * 除法運(yùn)算 * @param {number} num1 * @param {number} num2 * @returns {number} */ div(num1: number, num2: number): number { num1 = Number(num1); num2 = Number(num2); let t1 = 0, t2 = 0, dec1: number, dec2: number; try { t1 = this.countDecimals(num1); } catch (e) { } try { t2 = this.countDecimals(num2); } catch (e) { } dec1 = this.convertToInt(num1); dec2 = this.convertToInt(num2); const result = this.mul((dec1 / dec2), Math.pow(10, t2 - t1)); return this.getCorrectResult("div", num1, num2, result); } /** * 乘法運(yùn)算 * @param {number} num1 * @param {number} num2 * @returns {number} */ mul(num1: number, num2: number): number { num1 = Number(num1); num2 = Number(num2); let times = 0, s1 = num1.toString(), s2 = num2.toString(); try { times += this.countDecimals(s1); } catch (e) { } try { times += this.countDecimals(s2); } catch (e) { } const result = this.convertToInt(s1) * this.convertToInt(s2) / Math.pow(10, times); return this.getCorrectResult("mul", num1, num2, result); } /** * 計(jì)算小數(shù)位的長(zhǎng)度 * @param {*} num * @returns {number} */ private countDecimals(num: any): number { let len = 0; try { num = Number(num); let str = num.toString().toUpperCase(); if (str.split('E').length === 2) { // 科學(xué)記數(shù)法 let isDecimal = false; if (str.split('.').length === 2) { str = str.split('.')[1]; if (parseInt(str.split('E')[0]) !== 0) { isDecimal = true; } } let x = str.split('E'); if (isDecimal) { len = x[0].length; } len -= parseInt(x[1]); } else if (str.split('.').length === 2) { // 十進(jìn)制 if (parseInt(str.split('.')[1]) !== 0) { len = str.split('.')[1].length; } } } catch(e) { throw e; } finally { if (isNaN(len) || len < 0) { len = 0; } return len; } } /** * 將小數(shù)轉(zhuǎn)成整數(shù) * @param {*} num * @returns {*} */ private convertToInt (num: any): number { num = Number(num); let newNum = num; let times = this.countDecimals(num); let temp_num = num.toString().toUpperCase(); if (temp_num.split('E').length === 2) { newNum = Math.round(num * Math.pow(10, times)); } else { newNum = Number(temp_num.replace(".", "")); } return newNum; } /** * 確認(rèn)我們的計(jì)算結(jié)果無(wú)誤,以防萬(wàn)一 * @param {string} type * @param {number} num1 * @param {number} num2 * @param {number} result * @returns {number} */ private getCorrectResult(type: 'add' | 'sub' | 'div' | 'mul', num1: number, num2: number, result: number): number { let temp_result = 0; switch (type) { case "add": temp_result = num1 + num2; break; case "sub": temp_result = num1 - num2; break; case "div": temp_result = num1 / num2; break; case "mul": temp_result = num1 * num2; break; } if (Math.abs(result - temp_result) > 1) { return temp_result; } return result; } }
希望這個(gè)方法能夠幫助到遇到問(wèn)題的小伙伴們。
總結(jié)
JavaScript 中的浮點(diǎn)數(shù)丟失精度問(wèn)題是由底層表示方式引起的,因此在進(jìn)行重要的精確計(jì)算時(shí)需要格外小心。選擇合適的方法,如整數(shù)計(jì)算、使用專門的庫(kù)或小數(shù)點(diǎn)后截?cái)?,可以幫助我們?cè)趯?shí)際應(yīng)用中處理這些問(wèn)題,確保得到精確的結(jié)果。在不同場(chǎng)景中選擇適當(dāng)?shù)姆椒?,是程序員需要謹(jǐn)慎考慮的問(wèn)題,以避免潛在的錯(cuò)誤。
到此這篇關(guān)于JavaScript計(jì)算出現(xiàn)精度丟失問(wèn)題的解決方法的文章就介紹到這了,更多相關(guān)JavaScript精度丟失內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
webpack4從0搭建組件庫(kù)的實(shí)現(xiàn)
這篇文章主要介紹了webpack4從0搭建組件庫(kù)的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11JavaScript中鏈?zhǔn)秸{(diào)用之研習(xí)
方法鏈一般適合對(duì)一個(gè)對(duì)象進(jìn)行連續(xù)操作(集中在一句代碼)。一定程度上可以減少代碼量,缺點(diǎn)是它占用了函數(shù)的返回值。2011-04-04JavaScript 關(guān)于元素獲取焦點(diǎn)(隱藏元素與div)
關(guān)于元素獲取焦點(diǎn)要注意2個(gè)小問(wèn)題,需要的朋友可以參考下。2011-01-01javascript 從if else 到 switch case 再到抽象
大家覺(jué)得在接手遺留代碼時(shí),見(jiàn)到什么東東是最讓人感到不耐煩的?復(fù)雜無(wú)比的 UML ?我覺(jué)得不是。2010-07-07第九篇Bootstrap導(dǎo)航菜單創(chuàng)建步驟詳解
這篇文章主要介紹了Bootstrap導(dǎo)航菜單創(chuàng)建步驟詳解的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-06-06JavaScript實(shí)現(xiàn)列出數(shù)組中最長(zhǎng)的連續(xù)數(shù)
這篇文章主要介紹了JavaScript實(shí)現(xiàn)列出數(shù)組中最長(zhǎng)的連續(xù)數(shù)的方法及使用,需要的朋友可以參考下2014-12-12學(xué)習(xí)JavaScript設(shè)計(jì)模式(繼承)
這篇文章主要帶領(lǐng)大家學(xué)習(xí)JavaScript設(shè)計(jì)模式,其中重點(diǎn)介紹繼承,舉例說(shuō)明為什么需要繼承,對(duì)繼承進(jìn)行詳細(xì)剖析,感興趣的小伙伴們可以參考一下2015-11-11bootstrap select2插件用ajax來(lái)獲取和顯示數(shù)據(jù)的實(shí)例
今天小編就為大家分享一篇bootstrap select2插件用ajax來(lái)獲取和顯示數(shù)據(jù)的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-08-08