JS大坑之19位數的Number型精度丟失問題

Sako發表於2019-04-21

起源

最近在實現一個需求的時候,需要接入第三方的介面,先呼叫A介面,A介面返回的資料裡,有一個taskId,然後再使用這個taskId請求B介面,獲取最終需要的資料。

後端使用的是node,因此最開始使用的是request-promise這個包請求第三方介面,然而在獲取A介面返回的taskId之後,呼叫B介面之後,B介面的響應居然是系統錯誤!簡易程式碼如下

const rp = require('request-promise')
const { taskId } = await rp('https://xxx.com/A')
const options = {
     method: 'POST',
     uri: 'https://xxx.com/B',
     body: {
        taskId
    },
     json: true
}
const result = await rp(options) 
// {
//    "errorcode": "40001",
//    "message": "系統錯誤",
//    "status": "failed"
// }
複製程式碼

接著我使用postman請求A介面,獲取新的taskId,再用新的taskId請求B介面,結果卻是正常的!

我在反覆檢查程式碼,確認請求的引數都是正常的格式之後,一時陷入了無盡的沉思之中。。。

發現

在做了幾次嘗試之後,我發現使用node請求得到的taskId最後兩位數都是0,即1152921504735848700,而使用postman獲取的taskId,則是比較正常的是1152921504735848759,接著我在node控制檯做如下操作

JS大坑之19位數的Number型精度丟失問題
就是這麼一瞬間,頓悟了。A介面裡的taskId是個19位數字,而request-promise在將資料解析成json時,導致這個19位的數字丟失了精度,查了下資料,發現js的number型別有個最大安全值,即2的53次方(9007199254740992),超過這個值就會出現精度丟失的問題。 Orz

獲取正確的響應資料

由於在一開始使用request-promise包,因此獲取的taskId是丟失了精度了,因此改用了node原生的http模組傳送請求。

const req = https.request('https://xxx.com/A', (res) => {
    res.on('data', (chunk) => {
    // 由於這裡獲取到的響應資料是JSON字串,因此19位的數字只是字串的一部分,這時獲取到的taskId就是正確的數字
      console.log(`BODY: ${chunk}`);
    });
    res.on('end', () => {
      console.log('No more data in response.');
    })
  })
複製程式碼

雖然獲取到了正常的響應資料,但是這是個JSON字串,接下來還要把這個字串解析成JSON,但是用JSON.parse(),又會引起精度丟失的問題,這可真尷尬 Orz

如果這個介面是已方可控的,那麼就可以把這個19位數的number轉成字串,這樣在解析的時候就不會出錯了,但是由於是第三方介面,因此沒法改變。那麼最快的解決方案,就是換種程式語言請求啊╮(╯_╰)╭

最後的解決

好吧,最後還是用了node,不過我用了比較硬核的方案實現,先在獲取的JSON字串中,找到這個19位的數字,然後為它加上引號,這樣再用JSON.parse()解析的時候,就能保持正常的數值,這樣接下的流程就自然通了,程式碼如下

let result = '{"taskId":1152921504735848759,"status":"CREATED","progress":0.0,"success":true}'
// JSON.parse(result) 不為19位數補上雙引號,直接parse時,精度丟失,結果如下:
// { 
//   taskId: 1152921504735848700,
//   status: 'CREATED',
//   progress: 0,
//   success: true 
// }
const taskId = result.match(/[0-9]{19}/)[0] // 正則獲取19位數字的值
result = result.replace(taskId,`"${taskId}"`) // 補上雙引號
const data = JSON.parse(result) 
// { 
//   taskId: '1152921504735848759', // 解析出來之後是字串,因此沒有丟失精度
//   status: 'CREATED',
//   progress: 0,
//   success: true 
// }
複製程式碼

結語

使用node也有一段時間了,因為涉及不到大數計算,因此對於編號啊,ID啊,都是用字串形式進行儲存的,也就一直沒有遇到這個問題。這一次居然碰上了,不得不說js在這一方面確實有點弱勢,之後也嘗試了下使用Go,python進行請求,都是能正確解析。┑( ̄Д  ̄)┍ 不過node使用起來還是很舒服的,╮(╯▽╰)╭

Thanks!

相關文章