每一个Javascript程序员都应该了解的浮点知识



在Javascript开发人员的职业生涯中会碰到一些奇怪的错误——似乎基本的数学运算行不通。在这样或那样的时候,有人会告诉你实际上Javascript中的数字就是浮点。任何人试图去理解浮点或要搞清楚它们为什么如此奇怪,最终都会去看一篇 长而难懂的文章。而我这篇文章想用简单的方式让Javascript开发人员理解浮点。

这篇文章假定读者熟悉二进制与十进制间的转换(例如:1写成  1 2写成 10 b ,3是 11 b ,4是 100 b ...等等 )。去澄清更多的事情,在这篇文章中,词语“小数”主要是指机器中数字的十进制表示(例如:2.718)。词语“二进制”在这篇文章中指的是机器表示法。将会分别注明"基于十进制"或"基于二进制"。


浮点数表达方式 (以下简称浮点表示法)

去弄懂什么是浮点表示法,我们首先想到的是有非常多种类的数字,我们将逐步讲解。我们把1叫做整数——它是没有小数的整数。

1/2  叫做分数。它意味着整数1被分隔成两部分。这个分数的概念是非常重要的,用它来得到浮点表示法。

通常0.5被认为是一个小数。但是,一个非常重要的区别需要说明——实事上十进制小数0.5代表分数 1/2  , 0.5 1/2 十进制表示方式——在这篇文章中,我们叫它点记法。我们把0.5叫有限小数,因为小数点后面的部分是有限的——在0.5的数字5后面没有更多数字。当1/3用小数表示时就是一个无限小数,例如:0.3333....。再次强调下,这个概念在下面的讨论中非常重要。

这还有另一种方式来表示整数、分数和小数。你可能从来没见过这种方式。它大概是这样的: 6.022 x 10 23 (提示:这是一个阿佛加德罗常数,一个化学分子式 )。它通常被认为是标准表达式或科学表达式。这种表达式可以推算到这样:
D 1 .D 2 D 3 D 4 ...D p  x B E
这种形式叫做浮点表示法

数字   p   跟着D, D 1 .D 2 D 3 D 4 ...D p 叫有效数字或尾数。 p   是有效数字的数量,通常叫做精度。 x   跟随在尾数后面,它是表达式的一部分(在这篇文章中乘法符号用*表示)。基数后面紧随着的是指数 。指数可以是正数或负数。

浮点表示法之美在于它可以完全的表示任意数。例如:整数1可以用 1.0 x 10 0 表示,光速表示为 2.99792458 x 10 8 米每秒, 1/2可以用2进制表示: 0.1 x 2 0


移除小数点

在上面的实例中,我们仍然很难完全弄懂小数(数字中有小数点)。当用二进制表示小数时会出现一些问题。给出一个任意的浮点数,例如圆周率 π ,我们可以用浮点表示法为: 3.14159 x 10 0 。用二进制表示它是这样的:   11.00100100 001111... .。 假设这个数用16位方式去表示,这个数字在机器中会是这样: 11001001000011111 。现在问题是:小数点去哪了?这个尽然不包含指数(我们假定这是基于二进制的)。
如果数字是5.14159呢?整数部分应该是 101   ,代替上面的 11 ,需要多一个位字段 。当然我们可以指定第一个N位的字段属于整数部分(例如:小数点左边的部分),剩下的属于小数部分,不过这个知识点属于另一篇主题为定点数的文章。

一旦我们移除小数点,有两个东西可以利用:指数和尾数。我们可以通过公式转换来移除小数点,推算到浮点表示法:
D 1 D 2 D 3 D 4 ...D p  / (B p-1 ) x B E
这个格式是我们刚才得到的二进制浮点表示法 。注意到现在 有效数字是一个整数。这样做能让机器非常简单的存储浮点数。事实上,非常广泛的二进制浮点表达式是IEEE754标准。

IEEE754

Javascript中的浮点表达式格式指定为IEEE754。特别说明下,它是一个双精度格式,意思是给每一个浮点分配64位空间。虽然IEEE754不是唯一的二进制浮点表达式,但是它是使用最广泛的格式。在64位的二进制中它的格式如下:

可能要注意到浮点数机器表达式和我们写的表达式有一点点区别——这是惯例。在64位可用空间中,有一位用来标记——不管这个数是正数还是负数。11位用来放指数——这个允记的最大指数1024。剩下的52位分配给尾数。也许你一直奇怪在javascript中怎么会有 +0 和 -0 ,用符号位解释——所有的数字在Javascript中都有符号位。 Infinity  NaN  也是用浮点表达式编码的——带有一个 2047 的特殊指数。如果尾数是 0,它是一个正整数或负整数。如果不是,那么它是 NaN


舍入误差

上面浮点表达式已经介绍完毕,现在进入一个更棘手的问题——舍入误差。这是所有程序员编写浮点数相关代码时的痛苦来源,Javascript更是如此,因为在Javascript中只能用数字格式表示浮点数。

上面已经提到分数不能完全用十进制表示,例如:1/3。这个问题普遍存在。例如,在二进制中,1/10不能完全表示出来,它可以表示为 0.00110011001100110011...注意到这个0011 是无限循环的。这种特殊情况是因为发生了舍入误差。

首先,初步了解一下舍入误差。来看这个非常著名的无理数,派: 3.141592653589793...。非常多人记住了5个数(3.1415),这就是舍入的很好解释,下面我们会用到这个解释。舍入误差可以这样计算:

(R - A) / Bp-1

这里面 R 是取整数, A 是实数。 B 和我们先前提到的 p相同,它是表示精确度。所以这个难忘的派有了一个舍入误差: 0.00009265...

这个问题似乎不严重,让我们试试二进制数。用分数1/10。基于10进制,它写成0.1。用二进制它是:0.0011001100110011...。假定我们舍入到5个尾数,它是 0.0001。但是 0.0001在二进制中实际上是1/16(或者0.0625)!这意味这有一个0.0375的舍入误差,这个误差相当大。想象一下做一个基本的数学运算0.1+0.2,结果确是0.2625!


幸运的是,按照浮点表示法的规定,ECMAScrip指定了52位尾数,所以这个舍入误差非常小——这个规定特别详细说明了大部分数字的预计舍入误差。因为随着时间的推移执行浮点运算操作引发的错误会越来越多,IEEE 754标准也为数学运算指定了算法。


除了这些还有一点要注意。结合性的算法在处理浮点时不能保证正确,即使在高精度情况下。我的意思是 ((x + y) + a + b) 不一定等于 ((x + y) + (a + b))


而且这是Javascript程序员的痛苦来源。例如,在Javascipt中,0.1+0.2===0.3将得到false。希望你现在能理解这是为什么。当然还有更糟糕,事实上舍入误差在每一个连续的数学运算中都存在。

在JavaScript中处理浮点
在用Javascript 处理 数字方面已经有了大量的建议,有好的有坏的。大量的建议是在Javascript做算术运算前或后取整数。
在少数的建议中我见到过将所有数存成整数(不是类型)来操作,然后格式化去显示。例如:总价用分做单位。这有一个显然的问题——在这个世界上不是所有的国家的货币是小数(例如:毛里求斯币)。同时,有些国家的货币没有子单位(例如:日本 ),有些不是100的子单位(例如:阿拉伯币 ),或者多于一个子单位(例如:中国人民币)。最终,你只能再利用浮点——可能很差劲。

我见过最好的建议是用一些充分测试过的代码库去处理浮点,例如:   sinfuljs   或者   mathjs 我个人更喜欢mathjs(但是实际上,对于任何数学运算相关的计算我尽量远离Javascript)。 BigDecimal   也特别好用,用来处理任意精度数学运算。

另一个常见的建议是使用内置函数   toPrecision()     toFixed() 方法。任何想去使用它的人请注意——这些方法返回字符串格式。你可以看看下面的代码:

function foo(x, y) {
    return x.toPrecision() + y.toPrecision()
}

> foo(0.1, 0.2)
"0.10.2"

内置函数 toPrecision()  toFixed()方法目的是为了显示。小心使用。


总结

Javascript里的数字是真正的浮点。由于用二进制表示数字的不足,同时机器性能有限,我们只能得到有舍入误差的数据。这篇文章解释了舍入误差和为什么会有这种情况。总是使用这一个好的代码库代替自己去编写代码。




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值