當JavaScript遇上UINT64

  • 2019 年 12 月 4 日
  • 筆記

導語:寫下這篇文章的緣由是因為在項目過程中,碰到了一個使用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);

參考資料

Tags: