首先來講一個真實的案例。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
的數字是沒法準確提取的,更別說計算了。因此在接口通信中,請務必使用字符串形式。