javascript
javascript 数字精度问题
來源:http://rockyee.iteye.com/blog/891538
摘要:
由于計算機是用二進制來存儲和處理數字,不能精確表示浮點數,而JavaScript中沒有相應的封裝類來處理浮點數運算,直接計算會導致運算精度丟失。
為了避免產生精度差異,把需要計算的數字升級(乘以10的n次冪)成計算機能夠精確識別的整數,等計算完畢再降級(除以10的n次冪),這是大部分編程語言處理精度差異的通用方法。
關鍵詞:
計算精度 四舍五入 四則運算 精度丟失
1. 疑惑
我們知道,幾乎每種編程語言都提供了適合貨幣計算的類。例如C#提供了decimal,Java提供了BigDecimal,JavaScript提供了Number……
由于之前用decimal和BigDecimal用得很好,沒有產生過精度問題,所以一直沒有懷疑過JavaScript的Number類型,以為可以直接使用Number類型進行計算。但是直接使用是有問題的。
我們先看看四舍五入的如下代碼:
按正常結果,應該分別彈出0.01和162.30。但實際測試結果卻是在不同瀏覽器中得到的是不同的結果:
在ie6、7、8下得到0.00和162.30,第一個數截取不正確;
在firefox中得到0.01和162.29,第二個數截取不正確;
在opera下得到0.01和162.29,第二個數截取不正確
我們再來看看四則運算的代碼:
按正常結果,除第一行外(因為其本身就不能除盡),其他都應該要得到精確的結果,從彈出的結果我們卻發現不是我們想要的正確結果。是因為沒有轉換成Number類型嗎?我們轉換成Number后再計算看看:
alert(Number(1)/Number(3));//彈出: 0.3333333333333333 alert(Number(0.1) + Number(0.2));//彈出: 0.30000000000000004 alert(Number(-0.09) – Number(0.01));//彈出: -0.09999999999999999 alert(Number(0.012345) * Number(0.000001));//彈出: 1.2344999999999999e-8 alert(Number(0.000001) / Number(0.0001));//彈出: 0.009999999999999998還是一樣的結果,看來javascript默認把數字識別為number類型。為了驗證這一點,我們用typeof彈出類型看看:
alert(typeof(1));//彈出: number alert(typeof(1/3));//彈出: number alert(typeof(-0.09999999));//彈出: number2. 原因
為什么會產生這種精度丟失的問題呢?是javascript語言的bug嗎?
我們回憶一下大學時學過的計算機原理,計算機執行的是二進制算術,當十進制數不能準確轉換為二進制數時,這種精度誤差就在所難免。
再查查javascript的相關資料,我們知道javascript中的數字都是用浮點數表示的,并規定使用IEEE 754 標準的雙精度浮點數表示:
IEEE 754 規定了兩種基本浮點格式:單精度和雙精度。
IEEE單精度格式具有24 位有效數字精度(包含符號號),并總共占用32 位。
IEEE雙精度格式具有53 位有效數字精度(包含符號號),并總共占用64 位。
這種結構是一種科學表示法,用符號(正或負)、指數和尾數來表示,底數被確定為2,也就是說是把一個浮點數表示為尾數乘以2的指數次方再加上符號。下面來看一下具體的規格:
| ? | 符號位 ??????? | 指數位 ??????? | 小數部分 | 指數偏移量 |
| 單精度浮點數 | 1位(31) | 8位(30-23) | 23位(22-00) | 127 |
| 雙精度浮點數 | 1位(63) | 11位(62-52) | 52位(51-00) | 1023 |
指數是8位,可表達的范圍是0到255
而對應的實際的指數是-127到+128
這里特殊說明,-127和+128這兩個數據在IEEE當中是保留的用作多種用途的
-127表示的數字是0
128和其他位數組合表示多種意義,最典型的就是NAN狀態。
知道了這些,我們來模擬計算機的進制轉換的計算,就找一個簡單的0.1+0.2來推演吧(引用自 http://blog.csdn.net/xujiaxuliang/archive/2010/10/13/5939573.aspx): 十進制0.1 => 二進制0.00011001100110011…(循環0011) =>尾數為1.1001100110011001100…1100(共52位,除了小數點左邊的1),指數為-4(二進制移碼為00000000010),符號位為0 => 計算機存儲為:0 00000000100 10011001100110011…11001 => 因為尾數最多52位,所以實際存儲的值為0.00011001100110011001100110011001100110011001100110011001 而十進制0.2 => 二進制0.0011001100110011…(循環0011) =>尾數為1.1001100110011001100…1100(共52位,除了小數點左邊的1),指數為-3(二進制移碼為00000000011),符號位為0 => 存儲為:0 00000000011 10011001100110011…11001 因為尾數最多52位,所以實際存儲的值為0.00110011001100110011001100110011001100110011001100110011 那么兩者相加得: 0.00011001100110011001100110011001100110011001100110011001 + 0.00110011001100110011001100110011001100110011001100110011 = 0.01001100110011001100110011001100110011001100110011001100 轉換成10進制之后得到:0.30000000000000004
從上述的推演過程我們知道,這種誤差是難免的,c#的decimal和Java的BigDecimal之所以沒有出現精度差異,只是因為在其內部作了相應處理,把這種精度差異給屏蔽掉了,而javascript是一種弱類型的腳本語言,本身并沒有對計算精度做相應的處理,這就需要我們另外想辦法處理了。
3. 解決辦法
3.1 升級降級
從上文我們已經知道,javascript中產生精度差異的原因是計算機無法精確表示浮點數,連自身都不能精確,運算起來就更加得不到精確的結果了。那么怎么讓計算機精確認識要計算的數呢?
我們知道十進制的整數和二進制是可以互相進行精確轉換的,那么我們把浮點數升級(乘以10的n次冪)成計算機能夠精確識別的整數來計算,計算完畢之后再降級(除以10的n次冪),不就得到精確的結果了嗎?好,就這么辦!
我們知道,Math.pow(10,scale)可以得到10的scale次方,那么就把浮點數直接乘以Math.pow(10,scale)就可以了嗎?我最初就是這么想的,但后來卻發現一些數字運算后實際結果與我們的猜想并不一致。我們來看看這個簡單的運算:
按常理應該返回51206,但實際結果卻是51205.99999999999。奇怪吧?其實也不奇怪,這是因為浮點數不能精確參與乘法運算,即使這個運算很特殊(只是乘以10的scale次方進行升級)。如此我們就不能直接乘以10的scale次方進行升級,那就讓我們自己來挪動小數點吧。
怎么挪動小數點肯定大家是各有妙招,此處附上我寫的幾個方法:
這樣我們升級降級都可以轉換成字符串后調用String對象的自定義方法movePoint了,乘以10的scale次方我們傳正整數scale,除以10的scale次方我們傳負整數-scale。
再來看看我們之前升級512.06的代碼,采用自定義方法的調用代碼變成這樣:
這樣直接挪動小數點就不怕它不聽話出現一長串數字了(*^__^*)。 當然,movePoint方法得到的結果是字符串,如果要轉成Number類型也很方便(怎么轉就不再廢話了)。
?
?3.2 四舍五入
?好,有了升級降級的基礎,我們來看看四舍五入的方法,由于不同瀏覽器對Number的toFixed方法有不同的支持,我們需要用自己的方法去覆蓋瀏覽器的默認實現。
有一個簡單的辦法是我們自己來判斷要截取數據的后一位是否大于等于5,然后進行舍或者入。我們知道Math.ceil方法是取大于等于指定數的最小整數,Math.floor方法是取小于等于指定數的最大整數,于是我們可以利用這兩個方法來進行舍入處理,先將要進行舍入的數升級要舍入的位數scale(乘以10的scale次方),進行ceil或floor取整后,再降級要舍入的位數scale(除以10的scale次方)。
代碼如下
覆蓋Number類型的toFixed方法后,我們再來執行以下方法
在ie6、7、8、firefox、Opera下分別進行驗證,都能得到相應的正確的結果。
另一種方式是在網上找到的采用正則表達式來進行四舍五入,代碼如下:
經驗證,這兩個方法都能夠進行準確的四舍五入,那么采用哪個方法好呢?實踐出真知,我們寫一個簡單的方法來驗證一下兩種方式的性能:
?
function testRound() { var dt, dtBegin, dtEnd, i; dtBegin = new Date(); for (i=0; i<100000; i++) { dt = new Date(); Number("0." + dt.getMilliseconds()).toFixed(2); } dtEnd = new Date(); alert(dtEnd.getTime()-dtBegin.getTime()); }為了避免對同一個數字進行四舍五入運算有緩存問題,我們取當前毫秒數進行四舍五入。經驗證,在同一臺機器上運算10萬次的情況下,用movePoint方法,平均耗時2500毫秒;用正則表達式方法,平均耗時4000毫秒。
總結
以上是生活随笔為你收集整理的javascript 数字精度问题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Struts2源码阅读(六)_Actio
- 下一篇: 如何给正面的负反馈