當JavaScript遇上UINT64

導語:寫下這篇文章的緣由是因爲在項目過程中,碰到了一個使用JavaScript處理 UINT64 類型數字的坑。

與大部分現代編程語言(包括幾乎所有的腳本語言)一樣,JavaScript中的數字類型是基於 IEEE 754 標準來實現的,該標準通常也被稱爲“浮點數”。JavaScript使用的是“雙精度”格式(即64位二進制)。

較小的數值

不僅僅是JavaScript,所有遵循 IEEE 754 規範的語言都會碰到如下問題:

0.1 + 0.2 === 0.3; // false

從數學角度來說,上面的條件判斷結果應該是true,可實際上卻爲false。

這是因爲,二進制浮點數中的 0.1 和 0.2 並不是十分精確,它們相加的結果並非剛好等於 0.3 ,而是一個比較接近的數字 0.30000000000000004, 所以條件判斷的結果爲false。

那麼該如何處理這種語言上的缺陷呢?

最常見的方法是設置一個誤差範圍,通常稱爲“機器精度”(machine epsilon),對JavaScript的數字類型來說,這個值通常是2^-52(2.220446049250313e-16)。

從 ES6 開始,該值定義在Number.EPSILON中,我們可以直接拿來用,也可以爲 ES6 之前的版本寫polyfill:

if (!Number.EPSILON) {
    Number.EPSILON = Math.pow(2, -52);
}

可以使用Number.EPSILON來比較兩個數字是否相等(在指定的誤差範圍內):

function numbersCloseEnoughToEqual(n1, n2) {
    return Math.abs(n1 - n2) < Number.EPSILON;
}

let a = 0.1 + 0.2;
let b = 0.3;

numbersCloseEnoughToEqual(a, b); // true
numbersCloseEnoughToEqual(0.0000001, 0.0000002); // false

JavaScript中能夠呈現的最大浮點數大約是 1.798e+308(這是一個相當大的數字),它定義在 Number.MAX_VALUE中。最小浮點數定義在 Number.MIN_VALUE中,大約是5e-324,它不是負數,但無限接近於0!

JavaScript中整數的安全範圍

上述JavaScript數字的呈現方式決定了“整數”的安全範圍遠遠小於 Number.MAX_VALUE。

能夠被“安全”呈現的最大整數是2^53 - 1,即 9007199254740991,在 ES6 中被定義爲Number.MAX_SAFE_INTEGER。最小整數是 -9007199254740991,在 ES6 中被定義爲Number.MIN_SAFE_INTEGER。

實際上,在前端的應用場景中正負 2^52 - 1 是一個絕對夠用的安全整數範圍,然而在NodeJS的服務端開發中就不一定了,如數據庫中的64位ID(現在QQ號已經需要用UINT64來存儲了)。由於JavaScript的數字類型無法精確呈現64位的數值,所以比較將它們保存(轉換)爲字符串。

我遇到的坑

上個項目,在使用Protocol Buffer協議(下文簡稱PB協議)與其他語言的後臺服務通信的過程中(關於Protocol Buffer協議的介紹可以參考本人的這篇文章),需要將從A服務拿到一個UINT64類型(用戶帳號)的整數透傳給B服務。

其實之前也在PB協議中遇到過UINT64類型定義的字段,但是當這個UINT64整型小於Number.MAX_SAFE_INTEGER時,我們將它當作正常的Number類型處理是完全沒有問題的。不過,這次我遇到的UINT64字段的值全都大於Number.MAX_SAFE_INTEGER,這時我還將它當作Number類型來處理,導致B服務中根本查詢不到我傳過去的用戶帳號。

例如,我從A服務拿到的實際用戶帳號是144115197458450067,當我將它轉換成Number後,變成了144115197458450080,傳給B服務後,B服務告訴我係統中沒有這個用戶。。。沒有debug之前我還以爲是B服務出了bug,因爲我啥都沒做,就是數據透傳而已啊!

解決方案

當我們確實需要在JavaScript中對大數值進行處理時,目前還是需要藉助相關的工具庫。

實際上在使用JavaScript進行PB通信時,我會使用ProtoBuf.js這個庫幫我處理pb到json的類型轉換,而ProtoBuf.js本身是依賴了一個工具庫 long.js 來對 int64uint64 進行處理,long.js 會將上述兩種類型轉換成long類型對象實例。long.js提供了很多API供我們操作,比如將long類型對象實例轉換成其他類型(Number,String,Buffer),或者將一個其它類型轉換成long類型對象實例,具體的API可參考 Long.js API

例如,當我從A服務拿到一個UINT64類型的值longValueFromA,此時並需要進行處理時

const Long = require('long'),

function longHandleToString(v) {
    if(Long.isLong(v)) {
        return v.toString(); // 正確
    }
    return v;
}

function longHandleToNumber(v) {
    if(Long.isLong(v)) {
        return v.toNumber(); // 錯誤!v很可能大於Number.MAX_SAFE_INTEGER,轉換成Number後會不精確。
    }
    return v;
}

// longValueFromA已經是一個long類型對象實例:Long { low: 2056032594, high: 33554434, unsigned: true}
longValue = longHandleToString(longValueFromA); // '144115198721823058' 正確!
// longValue = longHandleToNumber(longValueFromA); // 144115197458450080 錯誤!

然後再將longValue通過PB協議傳給B服務時,要做一次類型轉換,將string類型轉換成long類型對象實例。

longValueToB = Long.fromString(longValue, true);

參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章