詳解JavaScript中浮點(diǎn)數(shù)的精度計(jì)算
一、前言
JavaScript
只有一種數(shù)字類(lèi)型 Number
,而且在Javascript
中所有的數(shù)字都是以IEEE-754
標(biāo)準(zhǔn)格式表示的。即所有數(shù)字都是以 64 位浮點(diǎn)數(shù)形式儲(chǔ)存,即便整數(shù)也是如此。 所以我們?cè)诖蛴?1.00
這樣的浮點(diǎn)數(shù)的結(jié)果是 1
而非 1.00
。浮點(diǎn)數(shù)的精度問(wèn)題也不只是JavaScript
特有,因?yàn)橛行┬?shù)以二進(jìn)制表示位數(shù)是無(wú)窮的。
十進(jìn)制 | 二進(jìn)制 |
---|---|
0.1 | 0.0001 1001 1001 1001 ... |
0.2 | 0.0011 0011 0011 0011 ... |
0.3 | 0.0100 1100 1100 1100 ... |
0.4 | 0.0110 0110 0110 0110 ... |
0.5 | 0.1 |
0.6 | 0.1001 1001 1001 1001 ... |
比如 1.1,其程序?qū)嶋H上無(wú)法真正的表示 ‘1.1',而只能做到一定程度上的準(zhǔn)確,這是無(wú)法避免的精度丟失:1.09999999999999999
二、浮點(diǎn)數(shù)精度問(wèn)題
其實(shí),在JavaScript
中浮點(diǎn)數(shù)精度問(wèn)題還要復(fù)雜些,這里只給一些在Chrome
中測(cè)試數(shù)據(jù):
console.log(1.0-0.9 == 0.1) //false console.log(1.0-0.8 == 0.2) //false console.log(1.0-0.7 == 0.3) //false console.log(1.0-0.6 == 0.4) //true console.log(1.0-0.5 == 0.5) //true console.log(1.0-0.4 == 0.6) //true console.log(1.0-0.3 == 0.7) //true console.log(1.0-0.2 == 0.8) //true console.log(1.0-0.1 == 0.9) //true
那如何來(lái)避免這類(lèi) 1.0-0.9 != 0.1
的非bug型問(wèn)題發(fā)生呢?下面給出一種目前用的比較多的解決方案, 在判斷浮點(diǎn)運(yùn)算結(jié)果前對(duì)計(jì)算結(jié)果進(jìn)行精度縮小,因?yàn)樵诰瓤s小的過(guò)程總會(huì)自動(dòng)四舍五入:
(1.0-0.9).toFixed(digits) // toFixed() 精度參數(shù)digits須在0與20之間 console.log(parseFloat((1.0-0.9).toFixed(10)) === 0.1) //true console.log(parseFloat((1.0-0.8).toFixed(10)) === 0.2) //true console.log(parseFloat((1.0-0.7).toFixed(10)) === 0.3) //true console.log(parseFloat((11.0-11.8).toFixed(10)) === -0.8) //true
寫(xiě)成一個(gè)方法:
//通過(guò)isEqual工具方法判斷數(shù)值是否相等 function isEqual(number1, number2, digits){ digits = digits == undefined? 10: digits; // 默認(rèn)精度為10 return number1.toFixed(digits) === number2.toFixed(digits); } console.log(isEqual(1.0-0.7, 0.3)); //true //原型擴(kuò)展方式,更喜歡面向?qū)ο蟮娘L(fēng)格 Number.prototype.isEqual = function(number, digits){ digits = digits == undefined? 10: digits; // 默認(rèn)精度為10 return this.toFixed(digits) === number.toFixed(digits); } console.log((1.0-0.7).isEqual(0.3)); //true
接下來(lái),再來(lái)試試浮點(diǎn)數(shù)的運(yùn)算,
console.log(1.79+0.12) //1.9100000000000001 console.log(2.01-0.12) //1.8899999999999997 console.log(1.01*1.3) //1.3130000000000002 console.log(0.69/10) //0.06899999999999999
解決方案 - 先升冪再降冪,同時(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; 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)) return (arg1*m+arg2*m)/m } //給Number類(lèi)型增加一個(gè)add方法,調(diào)用起來(lái)更加方便。 Number.prototype.add = function (arg){ return accAdd(arg,this); } //減法函數(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); } //除法函數(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); } //乘法函數(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); }
結(jié)果如下:
console.log(accAdd(1.79, 0.12)); //1.91 console.log(accSub(2.01, 0.12)); //1.89 console.log(accDiv(0.69, 10)); //0.069<br>console.log(accMul(1.01, 1.3)); //1.313
改造之后,可以愉快地進(jìn)行浮點(diǎn)數(shù)加減乘除操作了~
三、整數(shù)精度問(wèn)題
在 Javascript
中,整數(shù)精度同樣存在問(wèn)題,先來(lái)看看問(wèn)題:
JavaScript
代碼:
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ù)有嚴(yán)格的數(shù)據(jù)類(lèi)型,所以精度誤差的問(wèn)題就顯得格外突出。
四、解決方案
類(lèi)庫(kù)
通常這種對(duì)精度要求高的計(jì)算都應(yīng)該交給后端去計(jì)算和存儲(chǔ),因?yàn)楹蠖擞谐墒斓膸?kù)來(lái)解決這種計(jì)算問(wèn)題。前端也有幾個(gè)不錯(cuò)的類(lèi)庫(kù):
4.1.1 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)大,易于使用。
4.1.2 decimal.js
為 JavaScript
提供十進(jìn)制類(lèi)型的任意精度數(shù)值。
4.1.3 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ù)等可以查看這里。
五、延伸閱讀
5.1 IEEE 754 標(biāo)準(zhǔn)
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ù)字,具體:
- 第0位:符號(hào)位, s 表示 ,0表示正數(shù),1表示負(fù)數(shù);
- 第1位到第11位:儲(chǔ)存指數(shù)部分, e 表示 ;
- 第12位到第63位:儲(chǔ)存小數(shù)部分(即有效數(shù)字),f 表示;
其中:符號(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)。
5.2 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è)方法以前很少用。
六、拓展閱讀
到此這篇關(guān)于詳解JavaScript中浮點(diǎn)數(shù)的精度計(jì)算的文章就介紹到這了,更多相關(guān)JavaScript浮點(diǎn)數(shù)精度計(jì)算內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
FireBug 調(diào)試JS入門(mén)教程 如何調(diào)試JS
這篇文章主要為大家介紹下通過(guò)firefox的FireBug調(diào)試JS,需要的朋友可以參考下2013-12-12Javascript中的函數(shù)聲明與函數(shù)表達(dá)式(奇技淫巧)
Javascript有很多有趣的用法,在Google Code Search里能找到不少,今天從火丁筆記看到的,非常不錯(cuò),推薦大家看下。2011-03-03javascript二維數(shù)組轉(zhuǎn)置實(shí)例
這篇文章主要介紹了javascript二維數(shù)組轉(zhuǎn)置方法,實(shí)例分析了數(shù)組行列交換的轉(zhuǎn)置技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-01-01利用CSS、JavaScript及Ajax實(shí)現(xiàn)高效的圖片預(yù)加載
圖片預(yù)加載想必大家都不陌生吧,實(shí)現(xiàn)預(yù)加載圖片有很多方法,包括使用CSS、JavaScript及兩者的各種組合。這些技術(shù)可根據(jù)不同設(shè)計(jì)場(chǎng)景設(shè)計(jì)出相應(yīng)的解決方案,十分高效2013-10-10小程序?qū)崿F(xiàn)授權(quán)登陸的解決方案
這篇文章主要介紹了小程序?qū)崿F(xiàn)授權(quán)登陸的解決方案,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-12-12JS實(shí)現(xiàn)的用來(lái)對(duì)比兩個(gè)用指定分隔符分割的字符串是否相同
這篇文章主要介紹了JS實(shí)現(xiàn)的用來(lái)對(duì)比兩個(gè)用指定分隔符分割的字符串是否相同,本文代碼為特殊需要而寫(xiě),需要的朋友可以參考下2014-09-09JS 截取字符串substr 和 substring方法的區(qū)別
JS 截取字符串substr 和 substring方法的區(qū)別,需要的朋友可以參考下,根據(jù)需要自行選擇。2009-10-10微信小程序?qū)崿F(xiàn)購(gòu)物頁(yè)面左右聯(lián)動(dòng)
這篇文章主要為大家詳細(xì)介紹了微信小程序?qū)崿F(xiàn)購(gòu)物頁(yè)面左右聯(lián)動(dòng),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-02-02