在初學編程的時候,數字類型老是把我整的一頭霧水,C 裏的各類 int, float, double 等等用起來好麻煩,數字就是數字,分那麼細幹嗎,寫代碼太累了。當我開始接觸 js,簡直開心壞了,終於不用關心究竟是啥類型了,終於能夠裸奔了,開心!然鵝,奔着奔着發現有點不太對勁,0.1 + 0.2 === 0.3 竟然是 false,當時我就崩潰了,鑑於水平有限,把這個定性成 js 浮點運算的 bug,如今再回頭來看看是怎麼回事:node
在 js 中 Number 類型實際上都是浮點數,是按照 IEEE 754 標準實現的。從 wiki 上看,這個標準定義了幾種內存存儲格式:編程
而 js 的版本使用了 binary64 即雙精度實型。在 binary64 格式中,數字以 64 位二進制存儲,其存儲格式以下所示:json
0 - 51 爲分數位,52 - 62 爲指數位,63 爲符號位。
按照 IEEE 754 的描述,在分數位以前,默認會有個隱藏位 1,有效數字將會被表示爲 1.f,其中 f 就是 52 位分數,在 js 中整個浮點數能夠用如下公式來表示:安全
指數部分有 11 位,在 JavaScript 中是從 1-2^{(11-1)} 即 -1023 開始的,因此指數位的十進制取值範圍爲 (-1023, 1024)。11 位二進制能夠表示的數字範圍 0 - 2047,所以在實現中,會用 1023 表示 0,用 2046 表示 1023。即公式中的指數值實際上是 exponent - 1023 獲得的。數據結構
有兩個 exponent 值比較特殊:0 和 2047。ui
當 exponent = 2047,而且 f 值爲 0 時,該值表示爲無窮大,但若是 f > 0,該值則爲 NaN。spa
當 exponent = 0,f 也爲 0 時,該值可能爲 +0 也可能爲 -0,由於符號是單獨用一個位去存的。code
到這裏 0.1 + 0.2 !== 0.3 也就能夠知道緣由了:0.1 和 0.2 都不能用 binary64 精確表示,精度的丟失致使最終計算結果產生了偏差。xml
int64 表示值介於 -2^63 ( -9,223,372,036,854,775,808) 到 2^63-1 (+9,223,372,036,854,775,807 )之間的整數。對象
而 js 中的 Number 雖然可以表示很大的數,可是一旦超過最大安全整數,就沒法再保證精度了。這個最大安全整數和最小安全整數能夠經過 Number.MAX_SAFE_INTEGER 和 Number.MIN_SAFE_INTEGER 拿到,其值分別爲 9007199254740991 和 -9007199254740991,即 (2^53) - 1。
很明顯,沒辦法精確的將 int64 中的所有數字轉換到 js 的 Number 中,這可怎麼辦呢?
不要慌,遇到問題老是有辦法解決的,咱們先看看 protobufjs 是怎麼解決這個問題的。
protocol buffers 是一種序列化數據結構的協議,咱們會在 proto 文件中定義好將要被使用的數據結構或接口。你能夠把 protocol buffers 理解成相似於 json 或者 xml 之類的東西,他們都是語言無關的,不一樣語言只要基於協議的規範,把數據按照 proto 文件的定義進行序列化和反序列化便可。而 protobufjs 就是在 js 中用來序列化和反序列化 protocol buffers 數據的一個實現。
當咱們基於 protocol buffers 協議在 nodejs 程序和程序傳遞數據時,就會遇到 int64 的問題,看下面這個例子:
// a.proto ... message User { int64 id = 1; } ...
在 proto 文件中定義了一個 User 數據結構,其 id 爲 int64,按照以前的瞭解,js 中的數字一旦超過最大安全整數,就會丟失精度,所以 protobufjs 提供了一個配置項,來控制程序如何處理這種大數:
var object = UserMessage.toObject(message, { longs: String, // longs as strings (requires long.js) });
依據這裏的參數,會把 user message 中全部的大整數轉成對應的字符串。
有些狀況,這個參數是沒辦法控制的:
// a.proto ... message ListCustomLoginIDsByUserIDsResponse { map<int64,string> external_ids = 1; } ... // 在 nodejs 中拿到的通過 protobufjs 處理事後的數據長這樣: { "\u0001\u0000\u0000\u0000\u0000\u0000\u0000\u0000": "test" }
說好的 map<int64, string> key 是 int64 咋變成這樣了?咱也不知道這堆奇怪的字符串是個啥。
帶着無數疑問翻了翻 protobufjs 的文檔,發現一些端倪:
Map fields are Object.<string,T> with the key being the string representation of the respective value or an 8 characters long binary hash string for Long-likes.
protobufjs 會把 protobuf 中的 map 類型的 key 的值轉成字符串或者 8 字符長的二進制 hash 字符串。
數了一下,還真是 8 段,不過實在想不明白爲何不直接轉成對應的字符串,翻了一遍提交歷史也沒找到緣由,做者確定有他本身的考慮吧,個人猜想是爲了跟 proto 中定義的類型保持一致,而 js 中又沒有 int64,因此做者用了這樣一組特殊的 hash 來表示。
知道了這個字符串是怎麼來的,再把它解回去就很簡單了,protobufjs 也暴露了相關的 API 來處理:
const { LongBits } = require('protobufjs').util LongBits.fromHash("\u0001\u0000\u0000\u0000\u0000\u0000\u0000\u0000").toNumber() // 1
protobufjs 依賴了 longjs 來處理這種大數,會把大數拆成低 32 位和高 32 位兩部分,longjs 中也經過各類位運算實現了字符串和 long 的相互轉換,以及 long 數字之間的各類運算,有興趣的話能夠去了解下。
BigInt 如今處在 ECMAScript 標準化過程當中的第四階段,能夠說已經肯定了會新增這個內置對象來處理 Number 精度不夠用的問題。
BigInt 會在數字後面加一個 」n「 來區分普通數字和 BigInt,舉個例子:
typeof 123 // "number" typeof 123n // "bigint"
使用起來也跟正常的 Number 同樣,不過在作數字運算的時候,必須保證運算符號兩邊都是 BigInt。
想了解更多能夠看下 v8 提供的新特性文檔:https://v8.dev/features/bigint
標準都支持了 BigInt,終於不用爲大整數發愁了。不過浮點數運算的精度依舊沒辦法保證,按照原文的說法,BigInt 會成爲實現 BigDecimal 的基礎,相信不遠的未來,js 中也能放心大膽的作各類運算了。