JavaScript中的浮点数 (IEEE 754)

8

在此输入图片描述

如果我理解正确的话,JavaScript数字总是以双精度浮点数的形式存储,遵循国际IEEE 754标准。这意味着它使用52位小数有效数字。但在上图中,似乎二进制下的0.57使用了54位。

另一件事是(如果我理解正确),二进制下的0.55也是一个重复的数字。但是为什么0.55 + 1 = 1.55(没有损失),而0.57 + 1 = 1.5699999999999998

4"


4
可能是浮点数运算是否有问题?的重复。 - phuzi
1
你正在应对不同浏览器中JS Math库实现的不确定性。此外,机器(硬件)架构可能会强制执行不同的内部表示标准。加上不同操作系统上浮点数计算实现的怪异性,你几乎没有希望一般性地回答你的问题。有了哪个处理器(CPU),哪个操作系统,哪个浏览器...拥有所有这些,你可能能够回答针对该特定组合的问题。 - Richard Uie
@RichardUie 您的评论也是一个答案 :) - hungneox
3
@RichardUie:JavaScript 实现了 ECMA-262,而 ECMA-262 规定了 Number 格式及其算术运算。这个问题中的操作在不同正确实现的 JavaScript 中并没有区别。 - Eric Postpischil
1
@phuzi:不,这不是重复的问题。这里所询问的显示行为是由于ECMA-262规范引起的,而不是由于浮点数通常引起的。 - Eric Postpischil
显示剩余5条评论
3个回答

9
这意味着它使用52位小数有效数字。但在上面的图片中,似乎0.57的二进制使用了54位。JavaScript的“Number”类型本质上是IEEE 754基本64位二进制浮点型,具有53位有效数字。52位编码在“尾数”字段中。前导位通过指数字段编码(指数字段为1-2046表示前导位为1,指数字段为0表示前导位为0,指数字段为2047用于无穷大或NaN)。你看到的0.57的值有53个有效位。“0.”的前导部分是由“toString”操作产生的;它不是数字的编码的一部分。但为什么0.55 + 1 = 1.55(没有损失),而0.57 + 1 = 1.5699999999999998?
当JavaScript使用其默认规则为显示格式化某些Number x时,这些规则会生成最短的十进制数字(在其有效数字中,不计入前导“0.”等装饰),以便在转换回Number格式时产生x。此规则的目的包括(a)始终确保显示唯一标识源值是哪个确切的Number值和(b)不使用比实现(a)更多的数字。因此,如果您从诸如.57之类的十进制数字开始并将其转换为Number,则会得到某个值x,该值是由于转换必须四舍五入为可表示为Number格式的数字而产生的结果。然后,当对x进行格式化以供显示时,您将获得原始数字,因为规则表明要生成最短的数字,以便转换回x自然地生成您开始使用的数字。

(但是这个x并不精确地代表0.57。最接近0.57的double略低于它;请参阅IEEE double计算器上它的十进制和二进制64表示)。

另一方面,当你执行某些操作,比如.57 + 1,你正在进行一些算术运算,产生了一个起始不是简单十进制数字的数y。因此,在为显示格式化这样一个数字时,规则可能需要使用更多的数字。换句话说,当你加上.571时,在Number格式中得到的结果并不是你从1.57得到的相同数字。所以,为了格式化.57 + 1的结果,JavaScript必须使用更多的数字来区分那个数字和你从1.57得到的数字——它们不同,必须以不同的方式显示。


如果0.57可以被精确地表示为double,则求和的预舍入结果将完全是1.57,因此1 + 0.57将舍入到与1.57相同的double
但事实并非如此,它实际上是1 + nearest_double(0.57)=1.569999999999999951150186916493(预舍入,不是double),它向下舍入为1.56999999999999984012788445398。这些数字表示法比我们需要区分尾数的1ulp(最后一位单位)甚至0.5 ulp最大舍入误差所需的位数多得多。

1.57 四舍五入得到 ~1.57000000000000006217248937901,因此不能用它来打印 1 + 0.57 的结果。该十进制字符串需要将该数字与相邻的binary64值区分开。


恰巧,.55 + 1 中出现的四舍五入与将 1.55 转换为 Number 后得到的数字相同,因此显示 .55 + 1 的结果将产生“1.55”。


1
我认为你没有明确指出最接近0.57的double并不完全代表该数字;有些人可能会忽略打印为0.57并不意味着它完全是0.57这一事实。这就是为什么1 + 0.57可能与1.57不同的原因;如果0.57可以被精确存储,那么需要四舍五入到最接近的double的总和将完全是1.57,与十进制字面值相同。 - Peter Cordes
@PeterCordes:随意编辑;我不介意。我会在明天下午之前一直使用手机,所以添加详细的示例会很繁琐。那时我会再看一遍。 - Eric Postpischil
完成,添加了 double(0.57)1 + double(0.57)(在转换为 double 前后进行四舍五入)和 double(1.57) 的十进制表示。 - Peter Cordes

3

toString(2)会打印出最后一个非零位之前的字符串。

1.57与1 + 0.57的二进制表示不同(但是得到1.57的结果并不是不可能的),
但是在二进制中,1 + 0.55等于1.55,如下面的代码片段所示:

console.log(1.57)
console.log(1.57.toString(2))
console.log((1+.57).toString(2))
console.log("1.32 + 0.25 = ",1.32 + .25)
console.log((1.32 + .25).toString(2))
console.log(1.55)
console.log(1.55.toString(2))
console.log((1+.55).toString(2))

请记住计算机是基于二进制数进行运算的,1.571.55 只是人类可以读懂的输出结果。


2
Number.prototype.toString粗略地实现了ES262规范的以下部分:
7.1.12.1 NumberToString(m)
让n、k和s成为整数,使得k ≥ 1,10 ** k-1 ≤ s < 10 ** k, s × 10 ** n-k的数字值为m, 并且k尽可能小。
因此,toString只是估计值,它不返回存储的确切字节。
在控制台中看到的也不是精确表示。

不,我知道0.55在二进制中也是一个循环数字。但我的问题是为什么JS能够很好地处理它。 - hungneox
你可能误解了我的意思。我不明白为什么它不显示0.55,而是像“0.57”一样显示0.54999998。 - hungneox
@hungneox 因为 toString() 将其估计为 0.55。 - Jonas Wilms
我猜与toString无关。1 + 0.55 = 1.55 - hungneox
@hungneox 控制台显示字符串(一堆字符)。 - Jonas Wilms
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接