alert(28443422041709109)
会输出什么?
背景
在上一篇博文里我记录了一个诡异的前后端数据不一致的问题,最终定位为前端js精度丢失。但只说了原因及结论并没有深入研究这个问题。
这一篇博文准备在此基础上,深入探寻一番,彻底弄清楚这个问题发生的本质。
引子
让我们先来看几个小问题热热身:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15## js环境下运行,输出结果是什么?
1. console.log(1 === 1.0);
2. console.log(0.1 + 0.2 === 0.3);
3. console.log(9007199254740992);
4. console.log(9007199254740992 + 1);
5. console.log(9007199254740992 + 2);
6. console.log(9007199254740992 + 3);
7. console.log(9007199254740992 + 4);
... ...
浏览器环境下运行下看看是不是有点颠覆了三观?
JS数值的基础知识
在JS中,所有数值都是以64位浮点数形式保存的,所以1和1.0是相同的,是同一个数值。
浮点数不是精确的数值,也正因为此,0.1+0.2
才不等于0.3
。
根据浮点数表示的标准:1
2
3第1位:符号位,0表示正数,1表示负数
第2-12位:指数部分
第13-64位:尾数部分(即有效数字)
符号位决定了正负,指数决定了数值大小,尾数决定了精度。
精度范围
其中有效数字第一位默认总是1,不保存在64位浮点数之中,也就是说有效数字总是1.xxx..
的形式,最长可能为52位。加上默认的1,js中提供的有效数字最长为53个二进制位。
JS中数值浮点数表示形式公式为:1
(-1)^符号位 * 1.xxx... * 2^指数位
因此精度最多只能表示到53个二进制位,即-(2^53 - 1) ~ 2^53
。超过该范围的数值不能被精确表示。
数值范围
指数部分11位,最大值为2047(2^11 - 1),则数值范围为2^-1023 ~ 2^1024
,超过该范围则无法表示。
分析问题
了解了JS中数值表示的方法后,我们回过头来分析具体的问题。
引言中alert(28443422041709109)
为什么会输出28443422041709108
呢?
转换为2进制为1100101000011010010010001000010111111100001101000110100
,这串二进制数字我们使用进制转换工具转换一下可以得到28443422041709108
,也就是说28443422041709108
可以精确表示,而28443422041709109
就不能精确表示了。为什么会这样,我们可以看一个更直观的例子:
让我们看一下引子中的几个问题:9007199254740992
是有效精度范围内最大的数,即2^53,二进制表示为100...0
,1后面跟53个0。9007199254740992 + 1
则表示为100...01
,使用浮点数表示法表示时,最后一位的1将由于超出位数被舍去,因此9007199254740992 + 1
和9007199254740992
的表示一致,因此也就相等了。
而9007199254740992 + 2
则表示为100..10
,使用浮点数表示法表示时,1被保留下来,因此9007199254740992 + 2
反而是可以精确表示的。
同样地,28443422041709108
可以被精确地表示,而28443422041709109
的最后两位被舍为了0,因此他们的二进制表示是一样的,也就造成了诡异的28443422041709108 === 28443422041709109
问题。
结论&解决方案
当数值使用浮点数表示法表示精度位数超过53位时,就会存在精度丢失。大整数能够精确表示的上限是9007199254740992
,超过则可能存在精度丢失。
JAVA的Long型整数超过了JS可以精确表示的大数范围,所以后端在涉及到和前端交互大数值时,建议使用String类型替换Long,否则可能会由于精度丢失导致产生奇怪的问题。
有时候项目中使用的类库中定义了Long型的大整数,由于封装特性我们没法直接修改,在跟前端交互时,吐出的数据一定要转为String返回给浏览器。目前只有交易订单号超过了位数,在处理订单号时一定要注意返回给浏览器时要将Long型转为String。