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

參考資料

浮點數

相關文章
相關標籤/搜索