日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢(xún)
選擇下列產(chǎn)品馬上在線(xiàn)溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問(wèn)題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營(yíng)銷(xiāo)解決方案
Node.js中遇到大數(shù)處理精度丟失如何解決?前端也適用!

在 JavaScript 中浮點(diǎn)數(shù)運(yùn)算時(shí)經(jīng)常出現(xiàn) 0.1+0.2=0.30000000000000004 這樣的問(wèn)題,除此之外還有一個(gè)不容忽視的大數(shù)危機(jī)(大數(shù)處理精度丟失)問(wèn)題。

這個(gè)問(wèn)題之前看大家在群里也聊了不止一次,周末在另一個(gè)「Nodejs技術(shù)棧-交流群」又聊到了這個(gè)問(wèn)題,當(dāng)時(shí)簡(jiǎn)單的在群里大家一塊討論了下,這種交流學(xué)習(xí)的氛圍是挺好的,下面是大家周末在群里的討論。

之前也分享過(guò)這個(gè)問(wèn)題,我在做個(gè)梳理分享給大家,前端也適用,因?yàn)榇蠹叶际峭婚T(mén)語(yǔ)言 JavaScript。

JavaScript 最大安全整數(shù)

在開(kāi)始本節(jié)之前,希望你能事先了解一些 JavaScript 浮點(diǎn)數(shù)的相關(guān)知識(shí),在上篇文章 JavaScript 浮點(diǎn)數(shù)之迷:0.1 + 0.2 為什么不等于 0.3? 中很好的介紹了浮點(diǎn)數(shù)的存儲(chǔ)原理、為什么會(huì)產(chǎn)生精度丟失(建議事先閱讀下)。

IEEE 754 雙精確度浮點(diǎn)數(shù)(Double 64 Bits)中尾數(shù)部分是用來(lái)存儲(chǔ)整數(shù)的有效位數(shù),為 52 位,加上省略的一位 1 可以保存的實(shí)際數(shù)值為 。

 
 
 
 
  1. Math.pow(2, 53) // 9007199254740992 
  2.  
  3. Number.MAX_SAFE_INTEGER // 最大安全整數(shù) 9007199254740991  
  4. Number.MIN_SAFE_INTEGER // 最小安全整數(shù) -9007199254740991  

只要不超過(guò) JavaScript 中最大安全整數(shù)和最小安全整數(shù)范圍都是安全的。

大數(shù)處理精度丟失問(wèn)題復(fù)現(xiàn)

例一

當(dāng)你在 Chrome 的控制臺(tái)或者 Node.js 運(yùn)行環(huán)境里執(zhí)行以下代碼后會(huì)出現(xiàn)以下結(jié)果,What?為什么我定義的 200000436035958034 卻被轉(zhuǎn)義為了 200000436035958050,在了解了 JavaScript 浮點(diǎn)數(shù)存儲(chǔ)原理之后,應(yīng)該明白此時(shí)已經(jīng)觸發(fā)了 JavaScript 的最大安全整數(shù)范圍。

 
 
 
 
  1. const num = 200000436035958034; 
  2. console.log(num); // 200000436035958050 

例二

以下示例通過(guò)流讀取傳遞的數(shù)據(jù),保存在一個(gè)字符串 data 中,因?yàn)閭鬟f的是一個(gè) application/json 協(xié)議的數(shù)據(jù),我們需要對(duì) data 反序列化為一個(gè) obj 做業(yè)務(wù)處理。

 
 
 
 
  1. const http = require('http'); 
  2.  
  3. http.createServer((req, res) => { 
  4.     if (req.method === 'POST') { 
  5.         let data = ''; 
  6.         req.on('data', chunk => { 
  7.             data += chunk; 
  8.         }); 
  9.  
  10.         req.on('end', () => { 
  11.             console.log('未 JSON 反序列化情況:', data); 
  12.              
  13.             try { 
  14.                 // 反序列化為 obj 對(duì)象,用來(lái)處理業(yè)務(wù) 
  15.                 const obj = JSON.parse(data); 
  16.                 console.log('經(jīng)過(guò) JSON 反序列化之后:', obj); 
  17.  
  18.                 res.setHeader("Content-Type", "application/json"); 
  19.                 res.end(data); 
  20.             } catch(e) { 
  21.                 console.error(e); 
  22.  
  23.                 res.statusCode = 400; 
  24.                 res.end("Invalid JSON"); 
  25.             } 
  26.         }); 
  27.     } else { 
  28.         res.end('OK'); 
  29.     } 
  30. }).listen(3000) 

運(yùn)行上述程序之后在 POSTMAN 調(diào)用,200000436035958034 這個(gè)是一個(gè)大數(shù)值。

以下為輸出結(jié)果,發(fā)現(xiàn)沒(méi)有經(jīng)過(guò) JSON 序列化的一切正常,當(dāng)程序執(zhí)行 JSON.parse() 之后,又發(fā)生了精度問(wèn)題,這又是為什么呢?JSON 轉(zhuǎn)換和大數(shù)值精度之間又有什么貓膩呢?

 
 
 
 
  1. 未 JSON 反序列化情況: { 
  2.         "id": 200000436035958034 
  3. 經(jīng)過(guò) JSON 反序列化之后: { id: 200000436035958050 } 

經(jīng)過(guò) JSON 反序列化之后: { id: 200000436035958050 }

這個(gè)問(wèn)題也實(shí)際遇到過(guò),發(fā)生的方式是調(diào)用第三方接口拿到的是一個(gè)大數(shù)值的參數(shù),結(jié)果 JSON 之后就出現(xiàn)了類(lèi)似問(wèn)題,下面做下分析。

JSON 序列化對(duì)大數(shù)值解析有什么貓膩?

先了解下 JSON 的數(shù)據(jù)格式標(biāo)準(zhǔn),Internet Engineering Task Force 7159,簡(jiǎn)稱(chēng)(IETF 7159),是一種輕量級(jí)的、基于文本與語(yǔ)言無(wú)關(guān)的數(shù)據(jù)交互格式,源自 ECMAScript 編程語(yǔ)言標(biāo)準(zhǔn).

https://www.rfc-editor.org/rfc/rfc7159.txt 訪(fǎng)問(wèn)這個(gè)地址查看協(xié)議的相關(guān)內(nèi)容。

我們本節(jié)需要關(guān)注的是 “一個(gè) JSON 的 Value 是什么呢?” 上述協(xié)議中有規(guī)定必須為 object, array, number, or string 四個(gè)數(shù)據(jù)類(lèi)型,也可以是 false, null, true 這三個(gè)值。

到此,也就揭開(kāi)了這個(gè)謎底,JSON 在解析時(shí)對(duì)于其它類(lèi)型的編碼都會(huì)被默認(rèn)轉(zhuǎn)換掉。對(duì)應(yīng)我們這個(gè)例子中的大數(shù)值會(huì)默認(rèn)編碼為 number 類(lèi)型,這也是造成精度丟失的真正原因。

大數(shù)運(yùn)算的解決方案

1. 常用方法轉(zhuǎn)字符串

在前后端交互中這是通常的一種方案,例如,對(duì)訂單號(hào)的存儲(chǔ)采用數(shù)值類(lèi)型 Java 中的 long 類(lèi)型表示的最大值為 2 的 64 次方,而 JS 中為 Number.MAX_SAFE_INTEGER (Math.pow(2, 53) - 1),顯然超過(guò) JS 中能表示的最大安全值之外就要丟失精度了,最好的解法就是將訂單號(hào)由數(shù)值型轉(zhuǎn)為字符串返回給前端處理,這是在工作對(duì)接過(guò)程中實(shí)實(shí)在在遇到的一個(gè)坑。

2. 新的希望 BigInt

Bigint 是 JavaScript 中一個(gè)新的數(shù)據(jù)類(lèi)型,可以用來(lái)操作超出 Number 最大安全范圍的整數(shù)。

創(chuàng)建 BigInt 方法一

一種方法是在數(shù)字后面加上數(shù)字 n

 
 
 
 
  1. 200000436035958034n; // 200000436035958034n 

創(chuàng)建 BigInt 方法二

另一種方法是使用構(gòu)造函數(shù) BigInt(),還需要注意的是使用 BigInt 時(shí)最好還是使用字符串,否則還是會(huì)出現(xiàn)精度問(wèn)題,看官方文檔也提到了這塊 github.com/tc39/proposal-bigint#gotchas--exceptions 稱(chēng)為疑難雜癥

 
 
 
 
  1. BigInt('200000436035958034') // 200000436035958034n 
  2.  
  3. // 注意要使用字符串否則還是會(huì)被轉(zhuǎn)義 
  4. BigInt(200000436035958034) // 200000436035958048n 這不是一個(gè)正確的結(jié)果 

檢測(cè)類(lèi)型

BigInt 是一個(gè)新的數(shù)據(jù)類(lèi)型,因此它與 Number 并不是完全相等的,例如 1n 將不會(huì)全等于 1。

 
 
 
 
  1. typeof 200000436035958034n // bigint 
  2.  
  3. 1n === 1 // false 

運(yùn)算

BitInt 支持常見(jiàn)的運(yùn)算符,但是永遠(yuǎn)不要與 Number 混合使用,請(qǐng)始終保持一致。

 
 
 
 
  1. // 正確 
  2. 200000436035958034n + 1n // 200000436035958035n 
  3.  
  4. // 錯(cuò)誤 
  5. 200000436035958034n + 1 
  6.                                 ^ 
  7.  
  8. TypeError: Cannot mix BigInt and other types, use explicit conversions 

BigInt 轉(zhuǎn)為字符串

 
 
 
 
  1. String(200000436035958034n) // 200000436035958034 
  2.  
  3. // 或者以下方式 
  4. (200000436035958034n).toString() // 200000436035958034 

與 JSON 的沖突

使用 JSON.parse('{"id": 200000436035958034}') 來(lái)解析會(huì)造成精度丟失問(wèn)題,既然現(xiàn)在有了一個(gè) BigInt 出現(xiàn),是否使用以下方式就可以正常解析呢?

 
 
 
 
  1. JSON.parse('{"id": 200000436035958034n}'); 

運(yùn)行以上程序之后,會(huì)得到一個(gè) SyntaxError: Unexpected token n in JSON at position 25 錯(cuò)誤,最麻煩的就在這里,因?yàn)?JSON 是一個(gè)更為廣泛的數(shù)據(jù)協(xié)議類(lèi)型,影響面非常廣泛,不是輕易能夠變動(dòng)的。

在 TC39 proposal-bigint 倉(cāng)庫(kù)中也有人提過(guò)這個(gè)問(wèn)題 github.comtc39/proposal-bigint/issues/24 截至目前,該提案并未被添加到 JSON 中,因?yàn)檫@將破壞 JSON 的格式,很可能導(dǎo)致無(wú)法解析。

BigInt 的支持

BigInt 提案目前已進(jìn)入 Stage 4,已經(jīng)在 Chrome,Node,F(xiàn)irefox,Babel 中發(fā)布,在 Node.js 中支持的版本為 12+。

BigInt 總結(jié)

我們使用 BigInt 做一些運(yùn)算是沒(méi)有問(wèn)題的,但是和第三方接口交互,如果對(duì) JSON 字符串做序列化遇到一些大數(shù)問(wèn)題還是會(huì)出現(xiàn)精度丟失,顯然這是由于與 JSON 的沖突導(dǎo)致的,下面給出第三種方案。

3. 第三方庫(kù)

通過(guò)一些第三方庫(kù)也可以解決,但是你可能會(huì)想為什么要這么曲折呢?轉(zhuǎn)成字符串大家不都開(kāi)開(kāi)心心的嗎,但是呢,有的時(shí)候你需要對(duì)接第三方接口,取到的數(shù)據(jù)就包含這種大數(shù)的情況,且遇到那種拒不改的,業(yè)務(wù)總歸要完成吧!這里介紹第三種實(shí)現(xiàn)方案。

還拿我們上面 大數(shù)處理精度丟失問(wèn)題復(fù)現(xiàn) 的第二個(gè)例子進(jìn)行講解,通過(guò) json-bigint 這個(gè)庫(kù)來(lái)解決。

知道了 JSON 規(guī)范與 JavaScript 之間的沖突問(wèn)題之后,就不要直接使用 JSON.parse() 了,在接收數(shù)據(jù)流之后,先通過(guò)字符串方式進(jìn)行解析,利用 json-bigint 這個(gè)庫(kù),會(huì)自動(dòng)的將超過(guò) 2 的 53 次方類(lèi)型的數(shù)值轉(zhuǎn)為一個(gè) BigInt 類(lèi)型,再設(shè)置一個(gè)參數(shù) storeAsString: true 會(huì)將 BigInt 自動(dòng)轉(zhuǎn)為字符串。

 
 
 
 
  1. const http = require('http'); 
  2. const JSONbig = require('json-bigint')({ 'storeAsString': true}); 
  3.  
  4. http.createServer((req, res) => { 
  5.   if (req.method === 'POST') { 
  6.     let data = ''; 
  7.     req.on('data', chunk => { 
  8.       data += chunk; 
  9.     }); 
  10.  
  11.     req.on('end', () => { 
  12.       try { 
  13.         // 使用第三方庫(kù)進(jìn)行 JSON 序列化 
  14.         const obj = JSONbig.parse(data) 
  15.         console.log('經(jīng)過(guò) JSON 反序列化之后:', obj); 
  16.  
  17.         res.setHeader("Content-Type", "application/json"); 
  18.         res.end(data); 
  19.       } catch(e) { 
  20.         console.error(e); 
  21.         res.statusCode = 400; 
  22.         res.end("Invalid JSON"); 
  23.       } 
  24.     }); 
  25.   } else { 
  26.     res.end('OK'); 
  27.   } 
  28. }).listen(3000) 

再次驗(yàn)證會(huì)看到以下結(jié)果,這次是正確的,問(wèn)題也已經(jīng)完美解決了!

 
 
 
 
  1. JSON 反序列化之后 id 值: { id: '200000436035958034' } 

json-bigint 結(jié)合 Request client

介紹下 axios、node-fetch、undici、undici-fetch 這些請(qǐng)求客戶(hù)端如何結(jié)合 json-bigint 處理大數(shù)。

模擬服務(wù)端

使用 BigInt 創(chuàng)建一個(gè)大數(shù)模擬服務(wù)端返回?cái)?shù)據(jù),此時(shí),若請(qǐng)求的客戶(hù)端不處理是會(huì)造成精度丟失的。

 
 
 
 
  1. const http = require('http'); 
  2. const JSONbig = require('json-bigint')({ 'storeAsString': true}); 
  3.  
  4. http.createServer((req, res) => { 
  5.   res.end(JSONbig.stringify({ 
  6.     num: BigInt('200000436035958034') 
  7.   })) 
  8. }).listen(3000) 

axios

創(chuàng)建一個(gè) axios 請(qǐng)求實(shí)例 request,其中的 transformResponse 屬性我們對(duì)原始的響應(yīng)數(shù)據(jù)做一些自定義處理。

 
 
 
 
  1. const axios = require('axios').default; 
  2. const JSONbig = require('json-bigint')({ 'storeAsString': true}); 
  3.  
  4. const request = axios.create({ 
  5.   baseURL: 'http://localhost:3000', 
  6.   transformResponse: [function (data) { 
  7.     return JSONbig.parse(data) 
  8.   }], 
  9. }); 
  10.  
  11. request({ 
  12.   url: '/api/test' 
  13. }).then(response => { 
  14.   // 200000436035958034 
  15.   console.log(response.data.num); 
  16. }); 

node-fetch

node-fetch 在 Node.js 里用的也不少,一種方法是對(duì)返回的 text 數(shù)據(jù)做處理,其它更便捷的方法沒(méi)有深入研究。

 
 
 
 
  1. const fetch = require('node-fetch'); 
  2. const JSONbig = require('json-bigint')({ 'storeAsString': true}); 
  3. fetch('http://localhost:3000/api/data') 
  4.   .then(async res => JSONbig.parse(await res.text())) 
  5.   .then(data => console.log(data.num)); 

undici

request 這個(gè)已標(biāo)記為廢棄的客戶(hù)端就不介紹了,再推薦一個(gè)值得關(guān)注的 Node.js 請(qǐng)求客戶(hù)端 undici,前一段也寫(xiě)過(guò)一篇文章介紹 request 已廢棄 - 推薦一個(gè)超快的 Node.js HTTP Client undici。

 
 
 
 
  1. const undici = require('undici'); 
  2. const JSONbig = require('json-bigint')({ 'storeAsString': true}); 
  3. const client = new undici.Client('http://localhost:3000'); 
  4. (async () => { 
  5.   const { body } = await client.request({ 
  6.     path: '/api', 
  7.     method: 'GET', 
  8.   }); 
  9.    
  10.   body.setEncoding('utf8'); 
  11.   let str = ''; 
  12.   for await (const chunk of body) { 
  13.     str += chunk; 
  14.   } 
  15.  
  16.   console.log(JSONbig.parse(str)); // 200000436035958034 
  17.   console.log(JSON.parse(str)); // 200000436035958050 精度丟失 
  18. })(); 

undici-fetch

undici-fetch 是一個(gè)構(gòu)建在 undici 之上的 WHATWG fetch 實(shí)現(xiàn),使用和 node-fetch 差不多。

 
 
 
 
  1. const fetch = require('undici-fetch'); 
  2. const JSONbig = require('json-bigint')({ 'storeAsString': true}); 
  3. (async () => { 
  4.   const res = await fetch('http://localhost:3000'); 
  5.   const json = JSONbig.parse(await res.text()); 
  6.   console.log(json.num); // 200000436035958034 
  7. })(); 

總結(jié)

本文提出了一些產(chǎn)生大數(shù)精度丟失的原因,同時(shí)又給出了幾種解決方案,如遇到類(lèi)似問(wèn)題,都可參考。還是建議大家在系統(tǒng)設(shè)計(jì)時(shí)去遵循雙精度浮點(diǎn)數(shù)的規(guī)范來(lái)做,在查找問(wèn)題的過(guò)程中,有看到有些使用正則來(lái)匹配,個(gè)人角度還是不推薦的,一是正則本身就是一個(gè)耗時(shí)的操作,二操作起來(lái)還要查找一些匹配規(guī)律,一不小心可能會(huì)把返回結(jié)果中的所有數(shù)值都轉(zhuǎn)為字符串,也是不可行的。

本文轉(zhuǎn)載自微信公眾號(hào)「Nodejs技術(shù)?!梗梢酝ㄟ^(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系Nodejs技術(shù)棧公眾號(hào)。


本文名稱(chēng):Node.js中遇到大數(shù)處理精度丟失如何解決?前端也適用!
當(dāng)前網(wǎng)址:http://www.dlmjj.cn/article/dphcjdd.html