問(wèn)題描述
成都創(chuàng)新互聯(lián)公司專(zhuān)業(yè)為企業(yè)提供歷城網(wǎng)站建設(shè)、歷城做網(wǎng)站、歷城網(wǎng)站設(shè)計(jì)、歷城網(wǎng)站制作等企業(yè)網(wǎng)站建設(shè)、網(wǎng)頁(yè)設(shè)計(jì)與制作、歷城企業(yè)網(wǎng)站模板建站服務(wù),10多年歷城做網(wǎng)站經(jīng)驗(yàn),不只是建網(wǎng)站,更提供有價(jià)值的思路和整體網(wǎng)絡(luò)服務(wù)。
在 JavaScript 中整數(shù)和浮點(diǎn)數(shù)都屬于 Number 數(shù)據(jù)類(lèi)型,所有數(shù)字都是以 64 位浮點(diǎn)數(shù)形式儲(chǔ)存,即便整數(shù)也是如此。 所以我們?cè)诖蛴?1.00 這樣的浮點(diǎn)數(shù)的結(jié)果是 1 而非 1.00 。在一些特殊的數(shù)值表示中,例如金額,這樣看上去有點(diǎn)變扭,但是至少值是正確了。然而要命的是,當(dāng)浮點(diǎn)數(shù)做數(shù)學(xué)運(yùn)算的時(shí)候,你經(jīng)常會(huì)發(fā)現(xiàn)一些問(wèn)題,舉幾個(gè)例子:
// 加法 ===================== // 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
問(wèn)題的原因
似乎是不可思議。小學(xué)生都會(huì)算的題目,JavaScript 不會(huì)?我們來(lái)看看其真正的原因。
JavaScript 里的數(shù)字是采用 IEEE 754 標(biāo)準(zhǔn)的 64 位雙精度浮點(diǎn)數(shù)。該規(guī)范定義了浮點(diǎn)數(shù)的格式,對(duì)于64位的浮點(diǎn)數(shù)在內(nèi)存中的表示,最高的1位是符號(hào)位,接著的11位是指數(shù),剩下的52位為有效數(shù)字,具體:
如圖:
符號(hào)位決定了一個(gè)數(shù)的正負(fù),指數(shù)部分決定了數(shù)值的大小,小數(shù)部分決定了數(shù)值的精度。 IEEE 754規(guī)定,有效數(shù)字第一位默認(rèn)總是1,不保存在64位浮點(diǎn)數(shù)之中。也就是說(shuō),有效數(shù)字總是1.xx…xx的形式,其中xx..xx的部分保存在64位浮點(diǎn)數(shù)之中,最長(zhǎng)可能為52位。因此,JavaScript提供的有效數(shù)字最長(zhǎng)為53個(gè)二進(jìn)制位(64位浮點(diǎn)的后52位+有效數(shù)字第一位的1)。
計(jì)算過(guò)程
比如在 JavaScript 中計(jì)算 0.1 + 0.2時(shí),到底發(fā)生了什么呢?
首先,十進(jìn)制的0.1和0.2都會(huì)被轉(zhuǎn)換成二進(jìn)制,但由于浮點(diǎn)數(shù)用二進(jìn)制表達(dá)時(shí)是無(wú)窮的,例如。
0.1 -> 0.0001100110011001...(無(wú)限) 0.2 -> 0.0011001100110011...(無(wú)限)
IEEE 754 標(biāo)準(zhǔn)的 64 位雙精度浮點(diǎn)數(shù)的小數(shù)部分最多支持 53 位二進(jìn)制位,所以兩者相加之后得到二進(jìn)制為:
0.0100110011001100110011001100110011001100110011001100
因浮點(diǎn)數(shù)小數(shù)位的限制而截?cái)嗟亩M(jìn)制數(shù)字,再轉(zhuǎn)換為十進(jìn)制,就成了 0.30000000000000004。所以在進(jìn)行算術(shù)計(jì)算時(shí)會(huì)產(chǎn)生誤差。
整數(shù)的精度問(wèn)題
在 Javascript 中,整數(shù)精度同樣存在問(wèn)題,先來(lái)看看問(wèn)題:
console.log(19571992547450991); //=> 19571992547450990 console.log(19571992547450991===19571992547450992); //=> true
同樣的原因,在 JavaScript 中 Number類(lèi)型統(tǒng)一按浮點(diǎn)數(shù)處理,整數(shù)是按最大54位來(lái)算最大(253 - 1,Number.MAX_SAFE_INTEGER,9007199254740991) 和最小(-(253 - 1),Number.MIN_SAFE_INTEGER,-9007199254740991) 安全整數(shù)范圍的。所以只要超過(guò)這個(gè)范圍,就會(huì)存在被舍去的精度問(wèn)題。
當(dāng)然這個(gè)問(wèn)題并不只是在 Javascript 中才會(huì)出現(xiàn),幾乎所有的編程語(yǔ)言都采用了 IEEE-745 浮點(diǎn)數(shù)表示法,任何使用二進(jìn)制浮點(diǎn)數(shù)的編程語(yǔ)言都會(huì)有這個(gè)問(wèn)題,只不過(guò)在很多其他語(yǔ)言中已經(jīng)封裝好了方法來(lái)避免精度的問(wèn)題,而 JavaScript 是一門(mén)弱類(lèi)型的語(yǔ)言,從設(shè)計(jì)思想上就沒(méi)有對(duì)浮點(diǎn)數(shù)有個(gè)嚴(yán)格的數(shù)據(jù)類(lèi)型,所以精度誤差的問(wèn)題就顯得格外突出。
解決方案
上面說(shuō)了這么多問(wèn)題和原因,這里給出一些解決方案。
類(lèi)庫(kù)
通常這種對(duì)精度要求高的計(jì)算都應(yīng)該交給后端去計(jì)算和存儲(chǔ),因?yàn)楹蠖擞谐墒斓膸?kù)來(lái)解決這種計(jì)算問(wèn)題。前端也有幾個(gè)不錯(cuò)的類(lèi)庫(kù):
Math.js
Math.js 是專(zhuān)門(mén)為 JavaScript 和 Node.js 提供的一個(gè)廣泛的數(shù)學(xué)庫(kù)。它具有靈活的表達(dá)式解析器,支持符號(hào)計(jì)算,配有大量?jī)?nèi)置函數(shù)和常量,并提供集成解決方案來(lái)處理不同的數(shù)據(jù)類(lèi)型
像數(shù)字,大數(shù)字(超出安全數(shù)的數(shù)字),復(fù)數(shù),分?jǐn)?shù),單位和矩陣。 功能強(qiáng)大,易于使用。
官網(wǎng):http://mathjs.org/
GitHub:https://github.com/josdejong/mathjs
decimal.js
為 JavaScript 提供十進(jìn)制類(lèi)型的任意精度數(shù)值。
官網(wǎng):http://mikemcl.github.io/decimal.js/
GitHub:https://github.com/MikeMcl/decimal.js
big.js
官網(wǎng):http://mikemcl.github.io/big.js
GitHub:https://github.com/MikeMcl/big.js/
這幾個(gè)類(lèi)庫(kù)幫我們解決很多這類(lèi)問(wèn)題,不過(guò)通常我們前端做這類(lèi)運(yùn)算通常只用于表現(xiàn)層,應(yīng)用并不是很多。所以很多時(shí)候,一個(gè)函數(shù)能解決的問(wèn)題不需要引用一個(gè)類(lèi)庫(kù)來(lái)解決。
下面介紹各個(gè)更加簡(jiǎn)單的解決方案。
整數(shù)表示
對(duì)于整數(shù),我們可以通過(guò)用String類(lèi)型的表示來(lái)取值或傳值,否則會(huì)喪失精度。
格式化數(shù)字、金額、保留幾位小數(shù)等
如果只是格式化數(shù)字、金額、保留幾位小數(shù)等可以查看這里 https://www.jb51.net/article/165993.htm
浮點(diǎn)數(shù)運(yùn)算
toFixed() 方法
浮點(diǎn)數(shù)運(yùn)算的解決方案有很多,這里給出一種目前常用的解決方案, 在判斷浮點(diǎn)數(shù)運(yùn)算結(jié)果前對(duì)計(jì)算結(jié)果進(jìn)行精度縮小,因?yàn)樵诰瓤s小的過(guò)程總會(huì)自動(dòng)四舍五入。
toFixed() 方法使用定點(diǎn)表示法來(lái)格式化一個(gè)數(shù),會(huì)對(duì)結(jié)果進(jìn)行四舍五入。語(yǔ)法為:
numObj.toFixed(digits)
參數(shù) digits 表示小數(shù)點(diǎn)后數(shù)字的個(gè)數(shù);介于 0 到 20 (包括)之間,實(shí)現(xiàn)環(huán)境可能支持更大范圍。如果忽略該參數(shù),則默認(rèn)為 0。
返回一個(gè)數(shù)值的字符串表現(xiàn)形式,不使用指數(shù)記數(shù)法,而是在小數(shù)點(diǎn)后有 digits 位數(shù)字。該數(shù)值在必要時(shí)進(jìn)行四舍五入,另外在必要時(shí)會(huì)用 0 來(lái)填充小數(shù)部分,以便小數(shù)部分有指定的位數(shù)。 如果數(shù)值大于 1e+21,該方法會(huì)簡(jiǎn)單調(diào)用 Number.prototype.toString()并返回一個(gè)指數(shù)記數(shù)法格式的字符串。
特別注意:toFixed() 返回一個(gè)數(shù)值的字符串表現(xiàn)形式。
具體可以查看MDN中的說(shuō)明,那么我們可以這樣解決精度問(wèn)題:
parseFloat((數(shù)學(xué)表達(dá)式).toFixed(digits)); // toFixed() 精度參數(shù)須在 0 與20 之間 // 運(yùn)行 parseFloat((1.0 - 0.9).toFixed(10)) // 結(jié)果為 0.1 parseFloat((0.3 / 0.1).toFixed(10)) // 結(jié)果為 3 parseFloat((9.7 * 100).toFixed(10)) // 結(jié)果為 970 parseFloat((2.22 + 0.1).toFixed(10)) // 結(jié)果為 2.32
在老版本的IE瀏覽器(IE 6,7,8)中,toFixed()方法返回值不一定準(zhǔn)確。所以這個(gè)方法以前很少用。以至于網(wǎng)上搜索出來(lái)的結(jié)果大都是下面這些方法。
還有一些其他的解決方案,簡(jiǎn)單的說(shuō)需要將浮點(diǎn)數(shù)轉(zhuǎn)換字符串,分隔成為整數(shù)部分和小數(shù)部分,小數(shù)部分再轉(zhuǎn)換為整數(shù),計(jì)算結(jié)果后,再轉(zhuǎn)換為浮點(diǎn)數(shù)。這過(guò)程有點(diǎn)復(fù)雜…,網(wǎng)上找一下:
加法函數(shù)
/** ** 加法函數(shù),用來(lái)得到精確的加法結(jié)果 ** 說(shuō)明:javascript的加法結(jié)果會(huì)有誤差,在兩個(gè)浮點(diǎn)數(shù)相加的時(shí)候會(huì)比較明顯。這個(gè)函數(shù)返回較為精確的加法結(jié)果。 ** 調(diào)用:accAdd(arg1,arg2) ** 返回值:arg1加上arg2的精確結(jié)果 **/ 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類(lèi)型增加一個(gè)add方法,調(diào)用起來(lái)更加方便。 Number.prototype.add = function (arg) { return accAdd(arg, this); };
減法函數(shù)
/** ** 減法函數(shù),用來(lái)得到精確的減法結(jié)果 ** 說(shuō)明:javascript的減法結(jié)果會(huì)有誤差,在兩個(gè)浮點(diǎn)數(shù)相減的時(shí)候會(huì)比較明顯。這個(gè)函數(shù)返回較為精確的減法結(jié)果。 ** 調(diào)用:accSub(arg1,arg2) ** 返回值:arg1加上arg2的精確結(jié)果 **/ 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 //動(dòng)態(tài)控制精度長(zhǎng)度 n = (r1 >= r2) ? r1 : r2; return ((arg1 * m - arg2 * m) / m).toFixed(n); } // 給Number類(lèi)型增加一個(gè)mul方法,調(diào)用起來(lái)更加方便。 Number.prototype.sub = function (arg) { return accMul(arg, this); };
乘法函數(shù)
/** ** 乘法函數(shù),用來(lái)得到精確的乘法結(jié)果 ** 說(shuō)明:javascript的乘法結(jié)果會(huì)有誤差,在兩個(gè)浮點(diǎn)數(shù)相乘的時(shí)候會(huì)比較明顯。這個(gè)函數(shù)返回較為精確的乘法結(jié)果。 ** 調(diào)用:accMul(arg1,arg2) ** 返回值:arg1乘以 arg2的精確結(jié)果 **/ 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類(lèi)型增加一個(gè)mul方法,調(diào)用起來(lái)更加方便。 Number.prototype.mul = function (arg) { return accMul(arg, this); };
除法函數(shù)
/** ** 除法函數(shù),用來(lái)得到精確的除法結(jié)果 ** 說(shuō)明:javascript的除法結(jié)果會(huì)有誤差,在兩個(gè)浮點(diǎn)數(shù)相除的時(shí)候會(huì)比較明顯。這個(gè)函數(shù)返回較為精確的除法結(jié)果。 ** 調(diào)用:accDiv(arg1,arg2) ** 返回值:arg1除以arg2的精確結(jié)果 **/ 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類(lèi)型增加一個(gè)div方法,調(diào)用起來(lái)更加方便。 Number.prototype.div = function (arg) { return accDiv(this, arg); };
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持創(chuàng)新互聯(lián)。