JavaScript中错误舍入的大数字

643ylb08  于 2023-02-28  发布在  Java
关注(0)|答案(6)|浏览(151)

查看此代码:

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。
为什么会这样呢?

iyfjxgzm

iyfjxgzm1#

JavaScript的number类型的容量已经溢出,参见规范的§ 8.5节了解详细信息,这些ID必须是字符串。
IEEE-754双精度浮点数(JavaScript使用的数字类型)不能精确地表示所有数字(当然),众所周知,0.1 + 0.2 == 0.3是false,它可以像影响分数一样影响整数;当你的值超过9,007,199,254,740,991(Number.MAX_SAFE_INTEGER)时,它就会开始。
超越Number.MAX_SAFE_INTEGER + 19007199254740992),IEEE-754浮点格式不能再表示每个连续整数。9007199254740991 + 19007199254740992,但9007199254740992 + 1也是9007199254740992,因为9007199254740993不能用该格式表示。下一个可以是9007199254740994。那么9007199254740995就不能是,但是9007199254740996可以。
原因是我们已经用完了位,所以我们不再有1位;最低位现在表示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位精度)。指数的使用非常复杂(described here),但是用非常模糊的术语来说,如果我们给指数加1,则有效位的值会加倍,因为指数用于表示2的幂(再次提醒,这不是直接的,其中有一些技巧)。
我们来看一下9007199254740991(又名Number.MAX_SAFE_INTEGER)的值:

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

该指数值10000110011意味着每次我们向有效数加1,所表示的数字就增加1(整数1,我们很早就失去了表示小数的能力)。
但现在有效位已满,为了超过该数字,我们必须增加指数,这意味着如果我们在有效位上加1,则所表示的数字的值增加2,而不是1(因为指数应用于2,即该二进制浮点数的基数):

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

好吧,没关系,因为9007199254740991 + 1就是9007199254740992,但是,我们不能表示9007199254740993,我们已经没有位了,如果我们只在有效数上加1,它就会在值上加2:

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

这个格式不能再表示奇数了,因为我们增加了这个值,指数太大了。
最后,我们又用完了有效位,必须增加指数,所以我们只能表示4的倍数,然后是8的倍数,然后是16的倍数,依此类推。

ghhaqwfi

ghhaqwfi2#

你在这里看到的实际上是两次舍入的效果。ECMAScript中的数字在内部用双精度浮点表示。当id设置为714341252076979033时(十六进制0x9e9d9958274c359),则实际上会为其分配最接近的可表示双精度值,即7143412520769790720x9e9d9958274c380)。当您打印出该值时,它被四舍五入为15个有效的十进制数字,从而得到14341252076979100

svdrlsy4

svdrlsy43#

这个JSON解析器不会导致这种情况,只要尝试在fbug的控制台中输入714341252076979033,你就会看到相同的714341252076979100,详情请参见这篇博客文章:floating-point

nxowjjhe

nxowjjhe4#

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

ceil(lb 714341252076979033) = 60

位精确表示该值。
最接近的可精确表示的数字是714341252076979072(用二进制写原始数字,用0替换最后7位数字,并向上舍入,因为替换的最高位数字是1)。
你会得到714341252076979100而不是这个数字,因为ECMA-262 §9.8.1中描述的ToString()是10的幂,在53位精度下所有这些数字都相等。

h9a6wy2h

h9a6wy2h5#

问题是您的数字需要比JavaScript更高的精度。
你能把这个数字作为一个字符串发送吗?分成两部分?

vhmi4jdf

vhmi4jdf6#

JavaScript只能处理90亿以内的整数(即9加上15个零)。超过这个数字,你就会得到垃圾。解决这个问题的方法是使用字符串来保存数字。如果你需要用这些数字做数学运算,编写你自己的函数,或者看看你是否能找到一个函数库来保存它们:我建议使用前者,因为我不喜欢我所看到的库。为了让你开始,看看我的两个函数在另一个答案。

相关问题