前段时间在开发的过程中遇到一个奇怪的 Bug。
在服务端数据正常,前端页面渲染代码正常的情况下,浏览器页面渲染出的内容却不一样。
经过一番定位,最终在 Chrome 浏览器的控制台找到了线索。
在控制台里面查看到的情形是 response 和 preview 的值不一样
。
一、问题表现
preview 的结果截图
response 的结果截图
这就奇怪了,理论上来说 preview 和 response 都是同一份数据,怎么可能不一样呢?
然而事实就是如此。
1 | preview 返回 817809136971941000 |
于是,我通过 postman 发起请求,返回的数据和 response 的值一致。
后又将两个返回值和数据库里面的数据做了比对,同样发现 response 的值和后端数据库存储的是一样的。
也就是说 response 的值是对的,preview 的值是错的。
二、找到了原因并解决问题
经过网上搜索、讨论、以及各种测试,最终发现了问题的原因。
直接原因就是:后端对接口做了改造,将原本返回的 string 类型的 ID 改为了 Long 类型。
根本原因是:JavaScript 中 Number 类型在处理 Long 型的数值的时候,超过了一定限制之后就会出现精度丢失的情况。
前面的 preview 和 response 不一致的问题就是因为 preview 在显示的时候处理 response 的 Long类型的时候触发了精度丢失。
可以在浏览器控制台验证一下:
可以看出,变量 b 在输出的时候值变了,xx40993变为了xx41000。
继续在控制台验证,输出更多 Long 类型数字的情形:
1 | 817809136971940993 => 817809136971941000 // 18 位 |
可以看出,直接在控制台执行 Long 数值的时候,其执行结果千奇百怪。
其根本原因就是因为数字太长所以触发了 JS 数值类型的精度问题。
所以解决办法也很简单:让后端将其返回的 number 类型转换为 string 即可。
那么 Javascript 为什么会出现 Long 类型数值的精度问题呢?
三、Javascript 数值存储
Javascript 采用的是双精度浮点数存储的,每个数字占 8 个字节,即 64 个bit。
所以,JavaScript 中数值类型的精度是有限的,内部只有一种数字类型 Number。
所有数字都是采用 IEEE 754 标准定义的双精度 64 位格式存储,即使整数也是如此。
这就是说,JavaScript 语言实际上并没有真正的整数,所有数值都是小数(64 位浮点数)。
上图所示即为双精度浮点数的存储方式,途中划分了存储位,64 位格式存储其实际存储小数的有 52 位。
1 | 第 [63] 位 sign 表示符号位,1 bit,0 表示正数,1 表示负数。 |
能够通过 53 位小数位一一对应存储的数我们将其称为安全数字。
javascript 提供了查询安全数字的方法。
1 | console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991 (Math.pow(2, 53) - 1) |
在实际程序运行过程中,如果有数字超过了这个安全数字范围,则就会出现计算不准确的问题。
前面的 817809136971940993
有长度达到了17,很显然大于了 9007199254740991
,因此出现计算错误(精度丢失)也就能理解了。
四、0.1+0.2问题
javascript 数值计算有一个很经典的问题,0.1+0.2 === 0.30000000000000004
,其底层原因就是前面的双精度浮点数存储导致的。
计算过程如下:
1 | // Number(0.1).toString(2) 0.1 转换为二进制 |
通过上面一步一步计算可以看出,之所以0.1+0.2 === 0.30000000000000004
有三个原因:
1 | 1)javascript 的数值计算是将数字转换为二进制进行计算的。 |
经过一番精度截取之后再计算就导致了 0.1+0.2 != 0.3
了。
五、总结
精度丢失的根本问题就在于 Javascript 语言本身的数值类型采用的是“双精度浮点数”。
而“双精度浮点数”本身存储位只有 64 位,除去符号位、指数位之后就只剩下 52 位,再加上 1 位非显式存储位,总共 53 位。
即小数后面最多可以有52个1,最大值为 Math.pow(2, 53)-1
,超过这个值就没法存了,只能丢弃,也就是所谓的“精度丢失”。
超过 2^53-1 之后的数被称为不安全的数,因为此后只要指数相同,并且尾数前 52 位相同,则这个两个数数值相同(因为 52位之后的数被丢弃了)。