JavaScript里的数字问题

首先来讲一个真实的案例。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

float64js里是怎么存储的呢?这里其实也不是js的发明,而是遵循通用的IEEE 754(二进制浮点数算术)标准。这个标准是怎么规定的呢?考虑到float64太过复杂,咱们先来一个float14spa

上图是虚拟的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
复制代码

0d0b的意义不作赘述。按照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的数字是没法准确提取的,更别说计算了。因此在接口通信中,请务必使用字符串形式。

参考资料

浮点数

相关文章
相关标签/搜索