JavaScript 浮點數之迷:大數危機

在 JavaScript 中浮點數運算時常常出現 0.1+0.2=0.30000000000000004 這樣的問題,除了這個問題以外還有一個不容忽視的大數危機(大數處理丟失精度問題),也是近期遇到的一些問題,作下梳理同時理解下背後產生的緣由和解決方案。html

做者簡介:五月君,Nodejs Developer,慕課網認證做者,熱愛技術、喜歡分享的 90 後青年,歡迎關注 Nodejs技術棧 和 Github 開源項目 www.nodejs.red前端

JavaScript 最大安全整數

在開始本節以前,但願你能事先了解一些 JavaScript 浮點數的相關知識,在上篇文章 JavaScript 浮點數之迷:0.1 + 0.2 爲何不等於 0.3?中很好的介紹了浮點數的存儲原理、爲何會產生精度丟失(建議事先閱讀下)。node

IEEE 754 雙精確度浮點數(Double 64 Bits)中尾數部分是用來存儲整數的有效位數,爲 52 位,加上省略的一位 1 能夠保存的實際數值爲 [-(2^{53}-1), 2^{53}]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 序列化對大數值解析有什麼貓膩?

先了解下 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 類型,這也是形成精度丟失的真正緣由。

大數運算的解決方案

1. 經常使用方法轉字符串

在先後端交互中這是一般的一種方案,例如,對訂單號的存儲採用數值類型 Java 中的 long 類型表示的最大值爲 2 的 64 次方,而 JS 中爲 Number.MAX_SAFE_INTEGER (Math.pow(2, 53) - 1),顯然超過 JS 中能表示的最大安全值以外就要丟失精度了,最好的解法就是將訂單號由數值型轉爲字符串返回給前端處理,這是再和一個供應商對接過程當中實實在在遇到的一個坑。

2. 新的但願 BigInt

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 的衝突致使的,下面給出第三種方案。

3. 第三方庫

經過一些第三方庫也能夠解決,可是你可能會想爲何要這麼曲折呢?轉成字符串你們不都開開心心的嗎,可是呢,有的時候你須要對接第三方接口,取到的數據就包含這種大數的狀況,且遇到那種拒不改的,業務總歸要完成吧!這裏介紹第三種實現方案。

還拿咱們上面 大數處理精度丟失問題復現 的第二個例子進行講解,經過 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' }
複製代碼

總結

本文提出了一些產生大數精度丟失的緣由,同時又給出了幾種解決方案,如遇到相似問題,均可參考。仍是建議你們在系統設計時去遵循雙精度浮點數的規範來作,在查找問題的過程當中,有看到有些使用正則來匹配,我的角度仍是不推薦的,一是正則自己就是一個耗時的操做,二操做起來還要查找一些匹配規律,一不當心可能會把返回結果中的全部數值都轉爲字符串,也是不可行的。

Reference

v8.dev/features/bi… github.com/tc39/propos… en.wikipedia.org/wiki/Double…

相關文章
相關標籤/搜索