在 JavaScript 中浮點數運算時常常出現 0.1+0.2=0.30000000000000004 這樣的問題,除了這個問題以外還有一個不容忽視的大數危機(大數處理丟失精度問題),也是近期遇到的一些問題,作下梳理同時理解下背後產生的緣由和解決方案。html
做者簡介:五月君,Nodejs Developer,慕課網認證做者,熱愛技術、喜歡分享的 90 後青年,歡迎關注 Nodejs技術棧 和 Github 開源項目 www.nodejs.red前端
在開始本節以前,但願你能事先了解一些 JavaScript 浮點數的相關知識,在上篇文章 JavaScript 浮點數之迷:0.1 + 0.2 爲何不等於 0.3?中很好的介紹了浮點數的存儲原理、爲何會產生精度丟失(建議事先閱讀下)。node
IEEE 754 雙精確度浮點數(Double 64 Bits)中尾數部分是用來存儲整數的有效位數,爲 52 位,加上省略的一位 1 能夠保存的實際數值爲 。git
Math.pow(2, 53) // 9007199254740992
Number.MAX_SAFE_INTEGER // 最大安全整數 9007199254740991
Number.MIN_SAFE_INTEGER // 最小安全整數 -9007199254740991
複製代碼
只要不超過 JavaScript 中最大安全整數和最小安全整數範圍都是安全的。github
例一編程
當你在 Chrome 的控制檯或者 Node.js 運行環境裏執行如下代碼後會出現如下結果,What?爲何我定義的 200000436035958034 卻被轉義爲了 200000436035958050,在瞭解了 JavaScript 浮點數存儲原理以後,應該明白此時已經觸發了 JavaScript 的最大安全整數範圍。json
const num = 200000436035958034;
console.log(num); // 200000436035958050
複製代碼
例二後端
如下示例經過流讀取傳遞的數據,保存在一個字符串 data 中,由於傳遞的是一個 application/json 協議的數據,咱們須要對 data 反序列化爲一個 obj 作業務處理。安全
const http = require('http');
http.createServer((req, res) => {
if (req.method === 'POST') {
let data = '';
req.on('data', chunk => {
data += chunk;
});
req.on('end', () => {
console.log('未 JSON 反序列化狀況:', data);
try {
// 反序列化爲 obj 對象,用來處理業務
const obj = JSON.parse(data);
console.log('通過 JSON 反序列化以後:', obj);
res.setHeader("Content-Type", "application/json");
res.end(data);
} catch(e) {
console.error(e);
res.statusCode = 400;
res.end("Invalid JSON");
}
});
} else {
res.end('OK');
}
}).listen(3000)
複製代碼
運行上述程序以後在 POSTMAN 調用,200000436035958034 這個是一個大數值。app
如下爲輸出結果,發現沒有通過 JSON 序列化的一切正常,當程序執行 JSON.parse() 以後,又發生了精度問題,這又是爲何呢?JSON 轉換和大數值精度之間又有什麼貓膩呢?
未 JSON 反序列化狀況: {
"id": 200000436035958034
}
通過 JSON 反序列化以後: { id: 200000436035958050 }
複製代碼
這個問題也實際遇到過,發生的方式是調用第三方接口拿到的是一個大數值的參數,結果 JSON 以後就出現了相似問題,下面作下分析。
先了解下 JSON 的數據格式標準,Internet Engineering Task Force 7159,簡稱(IETF 7159),是一種輕量級的、基於文本與語言無關的數據交互格式,源自 ECMAScript 編程語言標準.
www.rfc-editor.org/rfc/rfc7159… 訪問這個地址查看協議的相關內容。
咱們本節須要關注的是 「一個 JSON 的 Value 是什麼呢?」 上述協議中有規定必須爲 object, array, number, or string 四個數據類型,也能夠是 false, null, true 這三個值。
到此,也就揭開了這個謎底,JSON 在解析時對於其它類型的編碼都會被默認轉換掉。對應咱們這個例子中的大數值會默認編碼爲 number 類型,這也是形成精度丟失的真正緣由。
在先後端交互中這是一般的一種方案,例如,對訂單號的存儲採用數值類型 Java 中的 long 類型表示的最大值爲 2 的 64 次方,而 JS 中爲 Number.MAX_SAFE_INTEGER (Math.pow(2, 53) - 1),顯然超過 JS 中能表示的最大安全值以外就要丟失精度了,最好的解法就是將訂單號由數值型轉爲字符串返回給前端處理,這是再和一個供應商對接過程當中實實在在遇到的一個坑。
Bigint 是 JavaScript 中一個新的數據類型,能夠用來操做超出 Number 最大安全範圍的整數。
建立 BigInt 方法一
一種方法是在數字後面加上數字 n
200000436035958034n; // 200000436035958034n
複製代碼
建立 BigInt 方法二
另外一種方法是使用構造函數 BigInt(),還須要注意的是使用 BigInt 時最好仍是使用字符串,不然仍是會出現精度問題,看官方文檔也提到了這塊 github.com/tc39/propos… 稱爲疑難雜症
BigInt('200000436035958034') // 200000436035958034n
// 注意要使用字符串不然仍是會被轉義
BigInt(200000436035958034) // 200000436035958048n 這不是一個正確的結果
複製代碼
檢測類型
BigInt 是一個新的數據類型,所以它與 Number 並非徹底相等的,例如 1n 將不會全等於 1。
typeof 200000436035958034n // bigint
1n === 1 // false
複製代碼
運算
BitInt 支持常見的運算符,可是永遠不要與 Number 混合使用,請始終保持一致。
// 正確
200000436035958034n + 1n // 200000436035958035n
// 錯誤
200000436035958034n + 1
^
TypeError: Cannot mix BigInt and other types, use explicit conversions
複製代碼
BigInt 轉爲字符串
String(200000436035958034n) // 200000436035958034
// 或者如下方式
(200000436035958034n).toString() // 200000436035958034
複製代碼
與 JSON 的衝突
使用 JSON.parse('{"id": 200000436035958034}') 來解析會形成精度丟失問題,既然如今有了一個 BigInt 出現,是否使用如下方式就能夠正常解析呢?
JSON.parse('{"id": 200000436035958034n}');
複製代碼
運行以上程序以後,會獲得一個 SyntaxError: Unexpected token n in JSON at position 25
錯誤,最麻煩的就在這裏,由於 JSON 是一個更爲普遍的數據協議類型,影響面很是普遍,不是輕易可以變更的。
在 TC39 proposal-bigint 倉庫中也有人提過這個問題 github.comtc39/proposal-bi… 截至目前,該提案並未被添加到 JSON 中,由於這將破壞 JSON 的格式,極可能致使沒法解析。
BigInt 的支持
BigInt 提案目前已進入 Stage 4,已經在 Chrome,Node,Firefox,Babel 中發佈,在 Node.js 中支持的版本爲 12+。
BigInt 總結
咱們使用 BigInt 作一些運算是沒有問題的,可是和第三方接口交互,若是對 JSON 字符串作序列化遇到一些大數問題仍是會出現精度丟失,顯然這是因爲與 JSON 的衝突致使的,下面給出第三種方案。
經過一些第三方庫也能夠解決,可是你可能會想爲何要這麼曲折呢?轉成字符串你們不都開開心心的嗎,可是呢,有的時候你須要對接第三方接口,取到的數據就包含這種大數的狀況,且遇到那種拒不改的,業務總歸要完成吧!這裏介紹第三種實現方案。
還拿咱們上面 大數處理精度丟失問題復現 的第二個例子進行講解,經過 json-bigint 這個庫來解決。
知道了 JSON 規範與 JavaScript 之間的衝突問題以後,就不要直接使用 JSON.parse() 了,在接收數據流以後,先經過字符串方式進行解析,利用 json-bigint 這個庫,會自動的將超過 2 的 53 次方類型的數值轉爲一個 BigInt 類型,再設置一個參數 storeAsString: true
會將 BigInt 自動轉爲字符串。
const http = require('http');
const JSONbig = require('json-bigint')({ 'storeAsString': true});
http.createServer((req, res) => {
if (req.method === 'POST') {
let data = '';
req.on('data', chunk => {
data += chunk;
});
req.on('end', () => {
try {
// 使用第三方庫進行 JSON 序列化
const obj = JSONbig.parse(data)
console.log('通過 JSON 反序列化以後:', obj);
res.setHeader("Content-Type", "application/json");
res.end(data);
} catch(e) {
console.error(e);
res.statusCode = 400;
res.end("Invalid JSON");
}
});
} else {
res.end('OK');
}
}).listen(3000)
複製代碼
再次驗證會看到如下結果,此次是正確的,問題也已經完美解決了!
JSON 反序列化以後 id 值: { id: '200000436035958034' }
複製代碼
本文提出了一些產生大數精度丟失的緣由,同時又給出了幾種解決方案,如遇到相似問題,均可參考。仍是建議你們在系統設計時去遵循雙精度浮點數的規範來作,在查找問題的過程當中,有看到有些使用正則來匹配,我的角度仍是不推薦的,一是正則自己就是一個耗時的操做,二操做起來還要查找一些匹配規律,一不當心可能會把返回結果中的全部數值都轉爲字符串,也是不可行的。
v8.dev/features/bi… github.com/tc39/propos… en.wikipedia.org/wiki/Double…