JavaScript 中错误地舍入了大数字

Large numbers erroneously rounded in JavaScript

提问人:Jaanus 提问时间:9/4/2009 最后编辑:double-beepJaanus 更新时间:10/17/2023 访问量:48297

问:

请参阅此代码:

var jsonString = '{"id":714341252076979033,"type":"FUZZY"}';
var jsonParsed = JSON.parse(jsonString);
console.log(jsonString, jsonParsed);

当我在 Firefox 3.5 中看到我的控制台时,值是四舍五入的数字:jsonParsed

Object id=714341252076979100 type=FUZZY

尝试了不同的值,相同的结果(四舍五入的数字)。

我也不明白它的四舍五入规则。714341252076979136 四舍五入为 714341252076979200,而 714341252076979135 四舍五入为 714341252076979100。

为什么会这样?

JavaScript 浮点 精度 IEEE-754

评论


答:

10赞 thorn0 9/4/2009 #1

此 JSON 解析器不会导致它。只需尝试进入 fbug 的控制台即可。你会看到相同的.有关详细信息,请参阅此博客文章:浮点714341252076979033714341252076979100

评论

8赞 Rick Regan 9/5/2009
感谢您链接到我的文章,但它只解释了一半的问题——内部四舍五入值的打印。即使 javascript 允许你打印整个内容,它仍然是错误的——它将是最接近的可表示双精度值,正如下面其他人所解释的那样。
105赞 T.J. Crowder 9/4/2009 #2

你溢出了 JavaScript 类型的容量,有关详细信息,请参阅规范的 §8.5IEEE-754 双精度二进制浮点的维基百科页面。这些 ID 必须是字符串。number

IEEE-754 双精度浮点数(JavaScript 使用的数字类型)不能精确表示所有数字(当然)。众所周知,这是错误的。这会影响整数,就像它影响小数一样;一旦您超过 9,007,199,254,740,991 (),它就开始了。0.1 + 0.2 === 0.3Number.MAX_SAFE_INTEGER

超出 () 后,IEEE-754 浮点格式不能再表示每个连续的整数。 是,但也是因为无法以格式表示。下一个可能是 .那么不能,但可以。Number.MAX_SAFE_INTEGER + 190071992547409929007199254740991 + 190071992547409929007199254740992 + 190071992547409929007199254740993900719925474099490071992547409959007199254740996

原因是我们已经用完了比特,所以我们不再有 1s 比特;最低阶位现在表示 2 的倍数。最终,如果我们继续前进,我们会失去那一点,只能以 4 的倍数工作。等等。

您的值高于该阈值,因此它们将四舍五入到最接近的可表示值。

从 ES2020 开始,您可以将 BigInt 用于任意大的整数,但它们没有 JSON 表示形式。您可以使用字符串和 reviver 函数:

const jsonString = '{"id":"714341252076979033","type":"FUZZY"}';
// Note it's a string −−−−^−−−−−−−−−−−−−−−−−−^

const obj = JSON.parse(jsonString, (key, value) => {
    if (key === "id" && typeof value === "string" && value.match(/^\d+$/)) {
        return BigInt(value);
    }
    return value;
});

console.log(obj);
(Look in the real console, the snippets console doesn't understand BigInt.)


如果你对这些位感到好奇,这里会发生以下情况:IEEE-754 二进制双精度浮点数有一个符号位、11 位指数(它定义了数字的整体比例,为 2 的幂 [因为这是一个二进制格式])和 52 位有效位(但这种格式非常聪明,它从这 52 位中获得 53 位的精度)。指数的使用方式很复杂(这里描述),但用非常模糊的术语来说,如果我们在指数上加 1,有效数的值就会加倍,因为指数用于 2 的幂(再次,需要注意的是,这不是直接的,那里有聪明)。

因此,让我们看一下值(又名):9007199254740991Number.MAX_SAFE_INTEGER

   +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− sign bit
  / +−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− exponent
 / /        |  +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+− significand
/ /         | /                                                  |
0 10000110011 1111111111111111111111111111111111111111111111111111
                = 9007199254740991 (Number.MAX_SAFE_INTEGER)

该指数值 ,意味着每当我们在有效数上加 1 时,所表示的数字就会增加 1(整数 1,我们很早就失去了表示小数的能力)。10000110011

但现在这个意义已经满了。要超过这个数字,我们必须增加指数,这意味着如果我们将 1 添加到有效数,则表示的数字的值将增加 2,而不是 1(因为指数应用于 2,即这个二进制浮点数的基数):

   +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− sign bit
  / +−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− exponent
 / /        |  +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+− significand
/ /         | /                                                  |
0 10000110100 0000000000000000000000000000000000000000000000000000
                = 9007199254740992 (Number.MAX_SAFE_INTEGER + 1)

嗯,没关系,因为无论如何都是。但!我们不能代表.我们已经用完了。如果我们只在有效数上加上 1,它就会在值上加 2:9007199254740991 + 190071992547409929007199254740993

   +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− sign bit
  / +−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− exponent
 / /        |  +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+− significand
/ /         | /                                                  |
0 10000110100 0000000000000000000000000000000000000000000000000001
                = 9007199254740994 (Number.MAX_SAFE_INTEGER + 3)

当我们增加值时,格式不再表示奇数,指数太大了。

最终,我们再次用完了有效位,必须增加指数,因此我们最终只能表示 4 的倍数。然后是 8 的倍数。然后是 16 的倍数。等等。

评论

6赞 jsh 4/3/2013
我喜欢这个答案,因为它实际上告诉你如何解决问题。
4赞 Esteban Küber 9/4/2009 #3

问题在于,你的数字需要比 JavaScript 更高的精度。

您可以将号码作为字符串发送吗?分成两部分?

72赞 Stephen Canon 9/4/2009 #4

你在这里看到的实际上是两次舍入的效果。ECMAScript 中的数字在内部表示为双精度浮点。当设置为 ( 以十六进制表示) 时,它实际上被分配了最接近的可表示双精度值,即 ()。当您打印出该值时,它将四舍五入为 15 位有效十进制数字,这给出 .id7143412520769790330x9e9d9958274c3597143412520769790720x9e9d9958274c38014341252076979100

评论

1赞 Monish Chhadwa 6/17/2019
我不明白的 15 位有效十进制数字“143412520769791”而不是“714341252076979”是怎么回事
1赞 user3125367 3/19/2021
这个答案似乎有两个错误:1)次要,最后一个数字缺少前导,2)主要,输出没有四舍五入到15位 - 它也是53位尾数浮点数的最接近的表示,它大约需要15.95个十进制数字。该部分不如四舍五入稳定,例如 errs into 和 errs into ,甚至这个 / limit 也会任意漂移。(迂腐模式:从某种意义上说,它是四舍五入,因为它“四舍五入”到小数点后 15.95 位)7...100...79135...79100...79136...79200...35...36
0赞 Sebastian Simon 9/10/2021
相关:为什么5726718050568503296在 JS 中被截断
5赞 Christoph 9/5/2009 #5

JavaScript 使用双精度浮点值,即总精度为 53 位,但您需要

ceil(lb 714341252076979033) = 60

位来精确表示值。

最接近的精确表示数字是(用二进制写原始数字,将最后 7 位数字替换为并向上舍入,因为替换的最高数字是 )。71434125207697907201

你会得到这个数字,而不是这个数字,因为正如 ECMA-262 所描述的,§9.8.1 以 10 的幂工作,在 53 位精度下,所有这些数字都是相等的。714341252076979100ToString()

2赞 Robert L 9/5/2009 #6

JavaScript 只能处理精确的整数,最多可达 90 亿(即 9 和 15 个零)。高于这个值,你就会得到垃圾。通过使用字符串来保存数字来解决此问题。如果你需要用这些数字做数学运算,写你自己的函数,或者看看你是否能为它们找到一个库:我建议前者,因为我不喜欢我见过的库。为了帮助您入门,请参阅我的两个函数在另一个答案中。