詳解JavaScript中浮點數(shù)的精度計算
一、前言
JavaScript
只有一種數(shù)字類型 Number
,而且在Javascript
中所有的數(shù)字都是以IEEE-754
標準格式表示的。即所有數(shù)字都是以 64 位浮點數(shù)形式儲存,即便整數(shù)也是如此。 所以我們在打印 1.00
這樣的浮點數(shù)的結(jié)果是 1
而非 1.00
。浮點數(shù)的精度問題也不只是JavaScript
特有,因為有些小數(shù)以二進制表示位數(shù)是無窮的。
十進制 | 二進制 |
---|---|
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上無法真正的表示 ‘1.1',而只能做到一定程度上的準確,這是無法避免的精度丟失:1.09999999999999999
二、浮點數(shù)精度問題
其實,在JavaScript
中浮點數(shù)精度問題還要復(fù)雜些,這里只給一些在Chrome
中測試數(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
那如何來避免這類 1.0-0.9 != 0.1
的非bug型問題發(fā)生呢?下面給出一種目前用的比較多的解決方案, 在判斷浮點運算結(jié)果前對計算結(jié)果進行精度縮小,因為在精度縮小的過程總會自動四舍五入:
(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
寫成一個方法:
//通過isEqual工具方法判斷數(shù)值是否相等 function isEqual(number1, number2, digits){ digits = digits == undefined? 10: digits; // 默認精度為10 return number1.toFixed(digits) === number2.toFixed(digits); } console.log(isEqual(1.0-0.7, 0.3)); //true //原型擴展方式,更喜歡面向?qū)ο蟮娘L格 Number.prototype.isEqual = function(number, digits){ digits = digits == undefined? 10: digits; // 默認精度為10 return this.toFixed(digits) === number.toFixed(digits); } console.log((1.0-0.7).isEqual(0.3)); //true
接下來,再來試試浮點數(shù)的運算,
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ù),用來得到精確的加法結(jié)果 //說明:javascript的加法結(jié)果會有誤差,在兩個浮點數(shù)相加的時候會比較明顯。這個函數(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類型增加一個add方法,調(diào)用起來更加方便。 Number.prototype.add = function (arg){ return accAdd(arg,this); } //減法函數(shù),用來得到精確的減法結(jié)果 //說明:javascript的加法結(jié)果會有誤差,在兩個浮點數(shù)相加的時候會比較明顯。這個函數(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 //動態(tài)控制精度長度 n=(r1>=r2)?r1:r2; return ((arg1*m-arg2*m)/m).toFixed(n); } //除法函數(shù),用來得到精確的除法結(jié)果 //說明:javascript的除法結(jié)果會有誤差,在兩個浮點數(shù)相除的時候會比較明顯。這個函數(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類型增加一個div方法,調(diào)用起來更加方便。 Number.prototype.div = function (arg){ return accDiv(this, arg); } //乘法函數(shù),用來得到精確的乘法結(jié)果 //說明:javascript的乘法結(jié)果會有誤差,在兩個浮點數(shù)相乘的時候會比較明顯。這個函數(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類型增加一個mul方法,調(diào)用起來更加方便。 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
改造之后,可以愉快地進行浮點數(shù)加減乘除操作了~
三、整數(shù)精度問題
在 Javascript
中,整數(shù)精度同樣存在問題,先來看看問題:
JavaScript
代碼:
console.log(19571992547450991); //=> 19571992547450990 console.log(19571992547450991===19571992547450992); //=> true
同樣的原因,在 JavaScript
中 Number
類型統(tǒng)一按浮點數(shù)處理,整數(shù)是按最大54位來算最大(253 - 1,Number.MAX_SAFE_INTEGER,9007199254740991)
和最小(-(253 - 1),Number.MIN_SAFE_INTEGER,-9007199254740991)
安全整數(shù)范圍的。所以只要超過這個范圍,就會存在被舍去的精度問題。
當然這個問題并不只是在 Javascript
中才會出現(xiàn),幾乎所有的編程語言都采用了 IEEE-745
浮點數(shù)表示法,任何使用二進制浮點數(shù)的編程語言都會有這個問題,只不過在很多其他語言中已經(jīng)封裝好了方法來避免精度的問題,而 JavaScript
作為一門弱類型語言,從設(shè)計思想上就沒有對浮點數(shù)有嚴格的數(shù)據(jù)類型,所以精度誤差的問題就顯得格外突出。
四、解決方案
類庫
通常這種對精度要求高的計算都應(yīng)該交給后端去計算和存儲,因為后端有成熟的庫來解決這種計算問題。前端也有幾個不錯的類庫:
4.1.1 Math.js
Math.js
是專門為 JavaScript
和 Node.js
提供的一個廣泛的數(shù)學庫。它具有靈活的表達式解析器,支持符號計算,配有大量內(nèi)置函數(shù)和常量,并提供集成解決方案來處理不同的數(shù)據(jù)類型 像數(shù)字,大數(shù)(超出安全數(shù)的數(shù)字),復(fù)數(shù),分數(shù),單位和矩陣。 功能強大,易于使用。
4.1.2 decimal.js
為 JavaScript
提供十進制類型的任意精度數(shù)值。
4.1.3 big.js
這幾個類庫幫我們解決很多這類問題,不過通常我們前端做這類運算通常只用于表現(xiàn)層,應(yīng)用并不是很多。所以很多時候,一個函數(shù)能解決的問題不需要引用一個類庫來解決。
下面介紹各個更加簡單的解決方案。
整數(shù)表示 對于整數(shù),我們可以通過用String
類型的表示來取值或傳值,否則會喪失精度。
格式化數(shù)字、金額、保留幾位小數(shù)等 如果只是格式化數(shù)字、金額、保留幾位小數(shù)等可以查看這里。
五、延伸閱讀
5.1 IEEE 754 標準
JavaScript
里的數(shù)字是采用 IEEE 754
標準的 64 位雙精度浮點數(shù)。該規(guī)范定義了浮點數(shù)的格式,對于64位浮點數(shù)在內(nèi)存中的表示,最高1位是符號位,接著的11位是指數(shù),剩下的52位為有效數(shù)字,具體:
- 第0位:符號位, s 表示 ,0表示正數(shù),1表示負數(shù);
- 第1位到第11位:儲存指數(shù)部分, e 表示 ;
- 第12位到第63位:儲存小數(shù)部分(即有效數(shù)字),f 表示;
其中:符號位決定了一個數(shù)的正負,指數(shù)部分決定了數(shù)值的大小,小數(shù)部分決定了數(shù)值的精度。
IEEE 754
規(guī)定,有效數(shù)字第一位默認總是1,不保存在64位浮點數(shù)之中。也就是說,有效數(shù)字總是1.xx…xx的形式,其中xx..xx的部分保存在64位浮點數(shù)之中,最長可能為52位。因此,JavaScript
提供的有效數(shù)字最長為53個二進制位(64位浮點的后52位+有效數(shù)字第一位的1)。
5.2 toFixed()
浮點數(shù)運算的解決方案有很多,這里給出一種目前常用的解決方案, 在判斷浮點數(shù)運算結(jié)果前對計算結(jié)果進行精度縮小,因為在精度縮小的過程總會自動四舍五入。
toFixed()
方法使用定點表示法來格式化一個數(shù),會對結(jié)果進行四舍五入。語法為:
numObj.toFixed(digits)
參數(shù) digits
表示小數(shù)點后數(shù)字的個數(shù);介于 0 到 20 (包括)之間,實現(xiàn)環(huán)境可能支持更大范圍。如果忽略該參數(shù),則默認為 0。
返回一個數(shù)值的字符串表現(xiàn)形式,不使用指數(shù)記數(shù)法,而是在小數(shù)點后有 digits
位數(shù)字。該數(shù)值在必要時進行四舍五入,另外在必要時會用 0 來填充小數(shù)部分,以便小數(shù)部分有指定的位數(shù)。 如果數(shù)值大于 1e+21,該方法會簡單調(diào)用 Number.prototype.toString()
并返回一個指數(shù)記數(shù)法格式的字符串。
注意:toFixed()
返回一個數(shù)值的字符串表現(xiàn)形式。
具體可以查看 MDN中的說明,那么我們可以這樣解決精度問題:
parseFloat((數(shù)學表達式).toFixed(digits)); // toFixed() 精度參數(shù)須在 0 與20 之間 // 運行 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()
方法返回值不一定準確。所以這個方法以前很少用。
六、拓展閱讀
到此這篇關(guān)于詳解JavaScript中浮點數(shù)的精度計算的文章就介紹到這了,更多相關(guān)JavaScript浮點數(shù)精度計算內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
FireBug 調(diào)試JS入門教程 如何調(diào)試JS
這篇文章主要為大家介紹下通過firefox的FireBug調(diào)試JS,需要的朋友可以參考下2013-12-12Javascript中的函數(shù)聲明與函數(shù)表達式(奇技淫巧)
Javascript有很多有趣的用法,在Google Code Search里能找到不少,今天從火丁筆記看到的,非常不錯,推薦大家看下。2011-03-03javascript二維數(shù)組轉(zhuǎn)置實例
這篇文章主要介紹了javascript二維數(shù)組轉(zhuǎn)置方法,實例分析了數(shù)組行列交換的轉(zhuǎn)置技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-01-01利用CSS、JavaScript及Ajax實現(xiàn)高效的圖片預(yù)加載
圖片預(yù)加載想必大家都不陌生吧,實現(xiàn)預(yù)加載圖片有很多方法,包括使用CSS、JavaScript及兩者的各種組合。這些技術(shù)可根據(jù)不同設(shè)計場景設(shè)計出相應(yīng)的解決方案,十分高效2013-10-10小程序?qū)崿F(xiàn)授權(quán)登陸的解決方案
這篇文章主要介紹了小程序?qū)崿F(xiàn)授權(quán)登陸的解決方案,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2018-12-12JS實現(xiàn)的用來對比兩個用指定分隔符分割的字符串是否相同
這篇文章主要介紹了JS實現(xiàn)的用來對比兩個用指定分隔符分割的字符串是否相同,本文代碼為特殊需要而寫,需要的朋友可以參考下2014-09-09JS 截取字符串substr 和 substring方法的區(qū)別
JS 截取字符串substr 和 substring方法的區(qū)別,需要的朋友可以參考下,根據(jù)需要自行選擇。2009-10-10微信小程序?qū)崿F(xiàn)購物頁面左右聯(lián)動
這篇文章主要為大家詳細介紹了微信小程序?qū)崿F(xiàn)購物頁面左右聯(lián)動,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-02-02