首先来讲一个真实的案例。html
某天测试提了个bug,大体是说编辑一个数据的时候,提示成功了但实际上没有生效。我一看是数据问题,心想那必然是后端的锅啊,直接就甩了出去。过了三分钟,后端又甩回来了,说你数据提交时传的id
根本不存在啊!不是我传给你的那个id
啊!git
他说的那个id
长这个样子:github
{
"id": 1663029898857414656,
...
}
复制代码
我心想我还能害你啊?谁没事儿会手贱去改这个id
啊?结果我抓了个包一看,传回去的id
还真跟拿到的id
不同。后端
传回去的id长这样:bash
{
"id": 1663029898857414700,
...
}
复制代码
我心想真是见了鬼了,一怒之下打了个log:网络
const n = 1663029898857414656;
console.log(n); // 1663029898857414700
复制代码
结果还真实出乎意料,打出来的数字和声明的数字不同。因而我开始了google之旅,获得的结果大体是这样的:测试
JavaScript里的数字都是float64,后端传过来的int64类型的数字,在大到必定程度的时候就会有精度丢失。ui
那么这个“大到必定程度”是多大呢?先说结果,是9007199254740991
。这个数字是怎么算出来的呢?咱们接下来就来一步一步地拆解。在这里首先声明,本文是综述,没有原创发明的部分,使用的图片也都来自网络,版权归原做者全部。google
float64
在js
里是怎么存储的呢?这里其实也不是js
的发明,而是遵循通用的IEEE 754
(二进制浮点数算术)标准。这个标准是怎么规定的呢?考虑到float64
太过复杂,咱们先来一个float14
。spa
上图是虚拟的float14
在内存中的存储方式。总体分为三段,第一段是符号位,第二段是指数位,第三段是尾数位。什么是指数位?什么是尾数位呢?这里要先说科学计数法。
科学记数法最先由阿基米德提出。在科学记数法中,一个数被写成一个实数a,与一个10的n次幂的积。在电脑或计算器中通常用e来表示10的幂,好比1.7e1,实际上就是17。
好了,在1.7e1
这个科学计数法里,1
就是指数,1.7
就是尾数。那么这个数字在float14里怎么存储呢?咱们须要先把十进制转化成二进制:
0d17 * 0d10^0d0
=> 0b10001 * 0b10^0b0
=> 0b0.10001 * 0b10^0b101
复制代码
0d
和0b
的意义不作赘述。按照IEEE 754的标准放到内存里,就变成了如下形式:
看上去so easy对吧!好,咱们再来一个0.25
练练手。首先咱们也把0.25
转换成二进制:
0d0.25
=> 0d1 * 0d2^(-0d2)
=> 0b1 * 0b10^(-0b10)
=> 0b0.1 × 0b10^(-0b1)
复制代码
如今问题来了,咱们的指数位是负的,那该怎么存呢?按照常规的想法,咱们会引入一个符号位,可是IEEE 754
标准没有这样作,而是使用了偏移指数的概念。所谓偏移指数,就是规定一个偏移值,好比16
,实际的指数要加上这个偏移值再填写到指数部分,这样比16
大的就表示正指数,比16
小的就表示负指数。要表示0.25
,指数部分应该填16-1=15
。这样一来0.25
就能够表示成:
这里仍然要先作二进制转换:
0d0.25
=> 0d1 * 0d2^(-0d2)
=> 0b1 * 0b10^(-0b10)
=> 0b0.1 × 0b10^(-0b1)
=> 0b0.1 × 0b10^(-0b1 + 0b10000)
=> 0b0.1 × 0b10^(0b1111)
复制代码
事情到这一步还不算完,咱们再考虑一个问题:浮点数17
既能够写成0b0.10001 * 0b10^0b101
,也能够写成0b0.010001 * 0b10^0b110
,那咱们究竟该用哪种呢?这就涉及到了一个惟一性问题。
为了解决惟一性问题,IEEE 754
引入了一个叫正规化的概念,即规定尾数部分的最高位必须是1
,也就是说尾数必须以0.1
开头。因为尾数部分的最高位必须是1
,这个1就没必要保存,能够节省一位提升精度。真是一箭双雕啊!
正规化后,17
就有了惟一的存储形式:
搞明白了float14
,咱们再来理解float64
就容易多了。float64
的存储形式以下:
float64
包含1
个符号位,11
个指数位,52
个尾数位。指数偏移量是1023
,取该值的缘由是为了保证正负指数能够对称。至此咱们就能够理解开篇提出的问题了,float64
的精度彻底取决于尾数,考虑到正规化因素,52
位能够存储的最大数字是:
2^53 - 1
=== 9007199254740991
=== Number.MAX_SAFE_INTEGER
复制代码
鉴于JavaScript
里只有float64
这一种数据类型,对于超过9007199254740991
的数字是没法准确提取的,更别说计算了。因此在接口通信中,请务必使用字符串形式。