JavaScript數字精度丟失問題總結

本文分爲三個部分javascript

  1. JS 數字精度丟失的一些典型問題
  2. JS 數字精度丟失的緣由
  3. 解決方案(一個對象+一個函數)

 

1、JS數字精度丟失的一些典型問題

 

1. 兩個簡單的浮點數相加html

1
0.1 + 0.2 != 0.3  // true

 

Firebug前端

這真不是 Firebug 的問題,能夠用alert試試 (哈哈開玩笑)。java

 

看看Java的運算結果git

 

再看看Python編程

 

2. 大整數運算瀏覽器

1
9999999999999999 == 10000000000000001  // ?

Firebugoracle

16位和17位數居然相等,沒天理啊。編程語言

 

又如ide

1
2
var  x = 9007199254740992
x + 1 == x  // ?

看結果

三觀又被顛覆了。

 

3. toFixed 不會四捨五入(Chrome)

1
1.335.toFixed(2)  // 1.33

Firebug

 

線上曾經發生過 Chrome 中價格和其它瀏覽器不一致,正是由於 toFixed 兼容性問題致使

 

2、JS 數字丟失精度的緣由

計算機的二進制實現和位數限制有些數沒法有限表示。就像一些無理數不能有限表示,如 圓周率 3.1415926...,1.3333... 等。JS 遵循 IEEE 754 規範,採用雙精度存儲(double precision),佔用 64 bit。如圖

 

意義

  • 1位用來表示符號位
  • 11位用來表示指數
  • 52位表示尾數

 

浮點數,好比

1
2
0.1 >> 0.0001 1001 1001 1001…(1001無限循環)
0.2 >> 0.0011 0011 0011 0011…(0011無限循環)

此時只能模仿十進制進行四捨五入了,可是二進制只有 0 和 1 兩個,因而變爲 0 舍 1 入。這便是計算機中部分浮點數運算時出現偏差,丟失精度的根本緣由。

 

大整數的精度丟失和浮點數本質上是同樣的,尾數位最大是 52 位,所以 JS 中能精準表示的最大整數是 Math.pow(2, 53),十進制即 9007199254740992。

大於 9007199254740992 的可能會丟失精度

1
2
3
9007199254740992     >> 10000000000000...000  // 共計 53 個 0
9007199254740992 + 1 >> 10000000000000...001  // 中間 52 個 0
9007199254740992 + 2 >> 10000000000000...010  // 中間 51 個 0

 

實際上

1
2
3
4
9007199254740992 + 1  // 丟失
9007199254740992 + 2  // 未丟失
9007199254740992 + 3  // 丟失
9007199254740992 + 4  // 未丟失

 

結果如圖

 

以上,能夠知道看似有窮的數字, 在計算機的二進制表示裏倒是無窮的,因爲存儲位數限制所以存在「捨去」,精度丟失就發生了。

想了解更深刻的分析能夠看這篇論文(又長又臭):What Every Computer Scientist Should Know About Floating-Point Arithmetic

 

3、解決方案

對於整數,前端出現問題的概率可能比較低,畢竟不多有業務須要須要用到超大整數,只要運算結果不超過 Math.pow(2, 53) 就不會丟失精度。

對於小數,前端出現問題的概率仍是不少的,尤爲在一些電商網站涉及到金額等數據。解決方式:把小數放到位整數(乘倍數),再縮小回原來倍數(除倍數)

1
2
// 0.1 + 0.2
(0.1*10 + 0.2*10) / 10 == 0.3  // true

  

如下是我寫了一個對象,對小數的加減乘除運算丟失精度作了屏蔽。固然轉換後的整數依然不能超過 9007199254740992。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
/**
  * floatObj 包含加減乘除四個方法,能確保浮點數運算不丟失精度
  *
  * 咱們知道計算機編程語言裏浮點數計算會存在精度丟失問題(或稱舍入偏差),其根本緣由是二進制和實現位數限制有些數沒法有限表示
  * 如下是十進制小數對應的二進制表示
  *      0.1 >> 0.0001 1001 1001 1001…(1001無限循環)
  *      0.2 >> 0.0011 0011 0011 0011…(0011無限循環)
  * 計算機裏每種數據類型的存儲是一個有限寬度,好比 JavaScript 使用 64 位存儲數字類型,所以超出的會捨去。捨去的部分就是精度丟失的部分。
  *
  * ** method **
  *  add / subtract / multiply /divide
  *
  * ** explame **
  *  0.1 + 0.2 == 0.30000000000000004 (多了 0.00000000000004)
  *  0.2 + 0.4 == 0.6000000000000001  (多了 0.0000000000001)
  *  19.9 * 100 == 1989.9999999999998 (少了 0.0000000000002)
  *
  * floatObj.add(0.1, 0.2) >> 0.3
  * floatObj.multiply(19.9, 100) >> 1990
  *
  */
var  floatObj =  function () {
    
     /*
      * 判斷obj是否爲一個整數
      */
     function  isInteger(obj) {
         return  Math.floor(obj) === obj
     }
    
     /*
      * 將一個浮點數轉成整數,返回整數和倍數。如 3.14 >> 314,倍數是 100
      * @param floatNum {number} 小數
      * @return {object}
      *   {times:100, num: 314}
      */
     function  toInteger(floatNum) {
         var  ret = {times: 1, num: 0}
         var  isNegative = floatNum < 0
         if  (isInteger(floatNum)) {
             ret.num = floatNum
             return  ret
         }
         var  strfi  = floatNum +  ''
         var  dotPos = strfi.indexOf( '.' )
         var  len    = strfi.substr(dotPos+1).length
         var  times  = Math.pow(10, len)
         var  intNum = parseInt(Math.abs(floatNum) * times + 0.5, 10)
         ret.times  = times
         if  (isNegative) {
             intNum = -intNum
         }
         ret.num = intNum
         return  ret
     }
    
     /*
      * 核心方法,實現加減乘除運算,確保不丟失精度
      * 思路:把小數放大爲整數(乘),進行算術運算,再縮小爲小數(除)
      *
      * @param a {number} 運算數1
      * @param b {number} 運算數2
      * @param digits {number} 精度,保留的小數點數,好比 2, 即保留爲兩位小數
      * @param op {string} 運算類型,有加減乘除(add/subtract/multiply/divide)
      *
      */
     function  operation(a, b, digits, op) {
         var  o1 = toInteger(a)
         var  o2 = toInteger(b)
         var  n1 = o1.num
         var  n2 = o2.num
         var  t1 = o1.times
         var  t2 = o2.times
         var  max = t1 > t2 ? t1 : t2
         var  result =  null
         switch  (op) {
             case  'add' :
                 if  (t1 === t2) {  // 兩個小數位數相同
                     result = n1 + n2
                 else  if  (t1 > t2) {  // o1 小數位 大於 o2
                     result = n1 + n2 * (t1 / t2)
                 else  // o1 小數位 小於 o2
                     result = n1 * (t2 / t1) + n2
                 }
                 return  result / max
             case  'subtract' :
                 if  (t1 === t2) {
                     result = n1 - n2
                 else  if  (t1 > t2) {
                     result = n1 - n2 * (t1 / t2)
                 else  {
                     result = n1 * (t2 / t1) - n2
                 }
                 return  result / max
             case  'multiply' :
                 result = (n1 * n2) / (t1 * t2)
                 return  result
             case  'divide' :
                 result = (n1 / n2) * (t2 / t1)
                 return  result
         }
     }
    
     // 加減乘除的四個接口
     function  add(a, b, digits) {
         return  operation(a, b, digits,  'add' )
     }
     function  subtract(a, b, digits) {
         return  operation(a, b, digits,  'subtract' )
     }
     function  multiply(a, b, digits) {
         return  operation(a, b, digits,  'multiply' )
     }
     function  divide(a, b, digits) {
         return  operation(a, b, digits,  'divide' )
     }
    
     // exports
     return  {
         add: add,
         subtract: subtract,
         multiply: multiply,
         divide: divide
     }
}();

 

toFixed的修復以下

1
2
3
4
5
6
7
// toFixed 修復
function  toFixed(num, s) {
     var  times = Math.pow(10, s)
     var  des = num * times + 0.5
     des = parseInt(des, 10) / times
     return  des +  ''
}

 

相關:

http://0.30000000000000004.com

http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

 

原文連接:https://www.cnblogs.com/snandy/p/4943138.html

相關文章
相關標籤/搜索