導語:寫下這篇文章的緣由是因爲在項目過程中,碰到了一個使用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 來對 int64
和 uint64
進行處理,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);