JavaScript 浮点数计算和比较

先看几个例子 (基于node 8.4环境的测试)

 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
> console.log(0.2 === 0.2)
true

> console.log(0.2 == 0.2)
true

> console.log(0.2 == 0.1 + 0.1)
true

> console.log(0.2 === 0.1 + 0.1)
true

> console.log(0.20 === 0.1 + 0.1)
true

> console.log(0.30 === 0.1 + 0.2)
false

> console.log(0.3 === 0.1 + 0.2)
false

> console.log(0.2 === 0.1 + 0.1)
true

> console.log(0.1 + 0.1)
0.2

> console.log(0.1 + 0.2)
0.30000000000000004

> console.log(0.2 + 0.2)
0.4

> console.log(0.2 + 0.3)
0.5

> console.log(0.2 + 0.4)
0.6000000000000001

这些例子显示js中浮点数计算并不是精确值, 因此并不能简单的判断浮点数是不是相等.原因在于 十进制到二进制的转换导致的精度问题! 同样的问题出现在C/C++,Java,Javascript中,准确的说:“使用了IEEE 754浮点数格式”来存储浮点类型(float 32,double 64)的任何编程语言都有这个问题!

# 原理:

IEEE 754浮点格式:它用科学记数法以底数为2的小数来表示浮点数。IEEE浮点数(共32位)用1位表示数字符号,用8位表示指数,用23位来表示尾数(即小数部分)。此处指数用移码存储,尾数则是原码(没有符号位)。之所以用移码是因为移码的负数的符号位为0,这可以保证浮点数0的所有位都是0。双精度浮点数(64位),使用1位符号位、11位指数位、52位尾数位来表示。

因为科学记数法有很多种方式来表示给定的数字,所以要规范化浮点数,以便用底数为2并且小数点左边为1的小数来表示(注意是二进制的,所以只要不为0则一定有一位为1),按照需要调节指数就可以得到所需的数字。例如:十进制的1.25 => 二进制的1.01 => 则存储时指数为0、尾数为1.01、符号位为0.(十进制转二进制)

# 实例

为什么“0.1+0.2=0.30000000000000004”?首先声明这是javascript语言计算的结果(注意Javascript的数字类型是以64位的IEEE 754格式存储的)。正如同十进制无法精确表示1/3(0.33333…)一样,二进制也有无法精确表示的值。例如1/10。64位浮点数情况下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
十进制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.0011001100110011001100110011001100110011001100110011001

两者相加:
0.00011001100110011001100110011001100110011001100110011001 + 0.0011001100110011001100110011001100110011001100110011001  = 0.01001100110011001100110011001100110011001100110011001111
转换成10进制之后得到:0.30000000000000004!

而  0.1 + 0.1 = 
0.00011001100110011001100110011001100110011001100110011001 + 
0.00011001100110011001100110011001100110011001100110011001 =
0.00110011001100110011001100110011001100110011001100110010

0.0011001100110011001100110011001100110011001100110011001 === 0.2

基于上基本了解了浮点数计算问题的由来, 当然关于IEEE 754标准可以wiki, 还有标准的四种舍入方式:

  • 舍入到最接近:舍入到最接近,在一样接近的情况下偶数优先(Ties To Even,这是默认的舍入方式):会将结果舍入为最接近且可以表示的值,但是当存在两个数一样接近的时候,则取其中的偶数(在二进制中式以0结尾的)。
  • 朝+∞方向舍入:会将结果朝正无限大的方向舍入。
  • 朝-∞方向舍入:会将结果朝负无限大的方向舍入。
  • 朝0方向舍入:会将结果朝0的方向舍入。

# 如何处理

关于建议推荐这篇 http://yanhaijing.com/javascript/2014/03/14/what-every-javascript-developer-should-know-about-floating-points/ 以下为引用:

设计处理JavaScript数字的问题,已经存在很多的建议,好坏参半。大多数这些建议都是在算数运算之前或之后完成取舍。

到目前位置我见过的寥寥无几的建议就是把运算数全部存储为整数(无类型),然后格式化显示。通过一个例子可以看出,在账户中大量储存的美分而不是美元(不知道举的例子是什么账户)。这里有一个值得注意的问题——不是世界上所有的货币都是十进制的(毛里求斯币:毛里求斯卢比是毛里求斯共和国的流通货币。币值有25、50、100、200、500、1000和2000。辅币单位为分)。同时,吐槽了日元和人名币……。最终,你会重新创建浮点——有可能。

我见过处理浮点数最好的建议是使用库,像sinfuljs或mathjs。我个人比较喜欢mathjs(但实际上,任何和数学相关的我甚至不会使用JavaScript去做)。当需要任意精度数学计算的时候,BigDecimal也是非常有用的。

另一个被多次重复的建议是使用内置的toPrecision()和toFixed()方法。使用他们时最容易犯得逻辑错误是忘记这些方法的返回值字符串。所以如果你像下面这样会得不到想要的结果:

1
2
3
4
5
6
function foo(x, y) {
    return x.toPrecision() + y.toPrecision()
}

> foo(0.1, 0.2)
"0.10.2"

设计内置方法toPrecision()和toFixed()的目的仅是用于显示。谨慎使用!

Licensed under CC BY-NC-SA 4.0