本文由雲+社群發表
相信大家在平常的 JavaScript 開發中,都有遇到過浮點數運算精度誤差的問題,比如 console.log(0.1+0.2===0.3)// false
。在 JavaScript 中,所有的數字包括整數和小數都是用 Number
型別來表示的。本文通過介紹 Number
的二進位制儲存標準來理解浮點數運算精度問題,和理解 Number
物件的 MAX_VALUE
等屬性值是如何取值的,最後介紹了一些常用的浮點數精度運算解決方案。
Number 的儲存標準
JavaScript Number 採用的是 IEEE 754 定義的 64 位雙精度浮點型來表示。具體的位元組分配可以先看一下引自維基百科的圖:
從上圖中可以看到,從高到低,64位被分成3段,分別是:
- sign: 符號位,佔 1 位;
- exponent: 指數位,佔 11 位;
- fraction: 有效數字位,佔 52 位。
指數位有 11 位,取值範圍是 0 到 2047。當指數位 e=0 或者 e=2017 時,根據有效數字位 f 是否為 0 ,具有不同的特殊含義,具體見下表:
對於常用的 normal number, 為了方便表示指數為負數的情況,所以,指數位數值大小做了一個 -1023 的偏移量。對於一個非 0 數字而言,,它的二進位制的科學計數法裡的第一位有效數字固定是 1。這樣,一個雙精度浮點型數字的值就是
對於 subnormal number,它可以用來表示更加接近於 0 的數,它特殊的地方是有效數字位的前面補充的是 0 而不是 1,且指數為偏移量是 -1022,所以值是:
Number 物件中的幾個屬性值
知道了 Number 是如何儲存之後,Number 物件的屬性是如何取值的就明朗了。
Number.MAX_VALUE:可表示的最大的數,顯然 e 和 f 都取最大時能表示的數最大,值為
Number.MIN_VALUE:可表示的最小的正數,用最小的 subnormal number 來表示。當 e = 0 ,f 的最後一位為 1,其他為 0 時最小,值為
Number.EPSILON : 表示 1 與 Number 可表示的大於 1 的最小的浮點數之間的差值。值為
Number.MAXSAFEINTEGER:表示在 JavaScript 中最大的安全整數。可以連續且精確被表示出來的整數成為安全整數,比如 2^54 就不是個安全整數,因為它和 2^54+1 兩個數的表示是完全一樣的,e=1077,f=0。 Math.pow(2,54)===Math.pow(2,54)+1// true
。整數轉化為二進位制後,小數點後是不會有數字的,而用二進位制的科學計數法表示時,小數點後最多保留 52 位,加上前置的一個 1,有 53 位數字,所以當一個數轉化二進位制時,如果位數超過 53 位,必然會截斷末尾的部分,即導致不能精確表示,即為不安全整數。所以最小的會被截斷的整數是 100...001=2^53+1(中間有52個0)。這個數設為 X,則比 X 小的整數都能被精確表示出來,再加上“連續”這個條件,所以 X-1 不是我們要的答案,X-2 才是。 Number.MAX_SAFE_INTEGER
最終值為
Number.MINSAFEINTEGER:表示在 JavaScript 中最小的安全整數,對 Number.MAX_SAFE_INTEGER
取負值即可,值為 -9007199254740991
為什麼0.1+0.2不等於0.3
現在看看 console.log(0.1+0.2===0.3)// false
這個問題,數字 0.1 轉化成二進位制是 0.0001100110011... 即 1.10011001...1001 * 2^-4 (小數部分有52位,即有13個1001迴圈)。由於第 53 位是 1,類似 10 進位制的四捨五入,二進位制是“零舍一入”,所以 0.1 的最終二進位制科學計數法表示是 1.10011001...1010 * 2^-4,即二進位制數值大小實際上是 0.000110011001...10011010。下面的程式碼驗證了這個值(列印出來的值,把最末尾的0去掉了):
var a = 0.1;console.log(a.toString(2)); //0.0001100110011001100110011001100110011001100110011001101
複製程式碼
同理十進位制數字 0.2 轉化為二進位制的最終值是 1.10011001...1010 * 2^-3 即 0.00110011...100111010;十進位制 0.3 轉化位二進位制的最終值是 1.00110011...0011 * 2^-2
var b = 0.2;console.log(b.toString(2)); //0.001100110011001100110011001100110011001100110011001101var c = 0.3;console.log(c.toString(2)); //0.010011001100110011001100110011001100110011001100110011
複製程式碼
所以,0.1+0.2 的值即為上面 0.1 和 0.2 對應的二進位制數值的相加,如下圖所示
上圖中,對所得的和,“零舍一入”保留 52 位有效小數就是最終的值:0.01001100...110100(第 53 位是 1 ,所以往前進了 1),如下程式碼所示。這個值與上文中的 0.3 的最終二進位制表示的值明顯不相同,即解釋了 0.1 + 0.2 不等於 0.3 的根本原因所在(實際上,這個值轉化為 10 進位制約等於 0.30000000000000004)。注:列印出來的長度是 54,因為有 52 位有效小數,前面是'0.01',長度是 4,最後去掉末尾的 2 個 0,所以最後列印出來的長度是 52+4-2 = 54。
var d = 0.1 + 0.2;console.log(d.toString(2)); //0.0100110011001100110011001100110011001100110011001101console.log(d.toString(2).length); // 54
複製程式碼
浮點數精度運算解決方案
關於 js 浮點數運算精度丟失的問題,不同場景可以有不同的解決方案。 1、如果只是用來展示一個浮點數的結果,則可以借用 Number 物件的 toFixed 和 parseFloat 方法。下面程式碼片段中,fixed 參數列示要保留幾位小數,可以根據實際場景調整精度。
function formatNum(num, fixed = 10) { return parseFloat(a.toFixed(fixed))}var a = 0.1 + 0.2;console.log(formatNum(a)); //0.3
複製程式碼
2、如果需要進行浮點數的加減乘除等運算,由上文可知,在小於 Number.MAXSAFEINTEGER 範圍的整數是可以被精確表示出來的,所以可以先把小數轉化為整數,運算得到結果後再轉化為對應的小數。比如兩個浮點數的加法:
function add(num1, num2) { var decimalLen1 = (num1.toString().split('.')[1] || '').length; //第一個引數的小數個數 var decimalLen2 = (num2.toString().split('.')[1] || '').length; //第二個引數的小數個數 var baseNum = Math.pow(10, Math.max(decimalLen1, decimalLen2)); return (num1 * baseNum + num2 * baseNum) / baseNum;}console.log(add(0.1 , 0.2)); //0.3
複製程式碼
參考資料
- en.wikipedia.org/wiki/IEEE_7…
- en.wikipedia.org/wiki/Double…
- en.wikipedia.org/wiki/Normal…
- en.wikipedia.org/wiki/Denorm…
此文已由作者授權騰訊雲+社群釋出