小小的腦袋充滿大大的疑惑
關于
0.1 + 0.2 === 0.3
,大家應該都知道這個結果是false。
而
0.2 + 0.3 === 0.5
得到的又是true,這到底是咋肥事?
到底哪些計算是成立的?哪些又是不成立的?為什麼?如何解決?
帶着這些問題開始我們的探究
// 成立的
0.1 + 0.3 === 0.4 // true
0.2 + 0.2 === 0.4 // true
// 不成立的
0.1 * 3 === 0.3 // false
0.2 * 6 === 1.2 // false
什麼是IEEE 754?
首先我們要知道,無論是整數還是浮點數,在cpu中都是通過轉為二進制計算的,整數轉為二進制還是很簡單的。那麼,如何将浮點數轉為二進制數?此時我們就要引入一個關鍵字:IEEE754(二進制浮點數算術标準)
關于什麼是IEEE 754,維基百科中的是這麼解釋的:
IEEE二進制浮點數算術标準(IEEE 754)是20世紀80年代以來最廣泛使用的浮點數運算标準,為許多CPU與浮點運算器所采用。
大部分程式設計語言都提供了IEEE浮點數格式與算術,但有些将其列為非必需的。
有此可知,不僅是JavaScript,隻要是使用了IEEE 754浮點數格式,來存儲浮點類型(float 32,double 64)的任何程式設計語言都有這個問題!下面是我用C++舉的例子:
知道了前因,下面我來看一下到底如何把浮點數轉為二進制!!!
浮點數轉為二進制
首先,JavaScript中的Number是采用64位存儲的,這64位由3部分組成,(S:符号位,Exponent:指數域,Fraction:尾數域)。
如下圖(從右到左):
綜合科學計數法,可以通過以下的計算公式表示:
(-1)s * 1.F * 2E-1023
其中S表示符号,0表示正數、1表示負數
1023為偏移量,32位單精度偏移量位127,下圖中的S、P、M隻是叫法不同,本文以維基百科中單詞(S、F、E)為準。
那麼S、F、E如何獲得呢?
結算過程
拿263.3舉例:
1.首先先擷取263的二進制,如下圖
可得出263的二進制為:100000111
2.再計算0.3的二進制,如下圖
根據規律我們可以看出1001是循環部分,是以得出0.3的二進制為:01001100110011001............
是以263.3的二進制表示為100000111.01001100110011001............
3. 計算S
由于263.3是正數,所有用0表示,故目前64位展示位
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
_ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ |
4. 計算E
由于要計算為1.F * 2n的形式,是以要把100000111.01001100110011001............ 小數點移到前面,成為1.0000011101001100110011001............
小數點移動了8位,即28
由公式E-1023=8,得出E為1031,我們将1031(1024+7)轉為二進制為:
10000000111
目前結果:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
1 | 1 | 1 | 1 | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ |
4. 計算F
目前計算的值為:1.0000011101001100110011001............
将最高位去掉,剩下的填滿52位即可
即F為:0000011101001100110011001100110011001100110011001100
因第53位為1,二進制中的四舍五入(零舍一入),故最終結果如下:
0000011101001100110011001100110011001100110011001101
5.最終結果
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
大家也可通過通過這個網站直接計算出SEM的值來做驗證。
0.1 + 0.2 !== 0.3
回到問題本身,我們可以通過計算0.1、0,2的二進制,經過相加計算就可以知道為什麼等式不成立了。
0.1的二進制
通過在 https://babbage.cs.qc.cuny.edu/IEEE-754.old/Decimal.html 中計算得出
2-4*1 .1001100110011001100110011001100110011001100110011010
0.2的二進制
2-3*1 .1001100110011001100110011001100110011001100110011010
計算0.1 + 0.2的二進制
得到值之後該怎麼處理呢?浮點數的加法是怎麼進行的?那就是先對齊、再計算。
對齊以高數位為準,我們此處的例子中-3為高位。是以需要将0.1的二進制處理下,向左移動一位。
移動前:
2-4*1 .1001100110011001100110011001100110011001100110011010
移動後:
2-3*0 .1100110011001100110011001100110011001100110011001101
然後我們計算兩個二進制數:
0 .1100110011001100110011001100110011001100110011001101 +
1 .1001100110011001100110011001100110011001100110011010 =
10.0110011001100110011001100110011001100110011001100111
因為我們計算的數的格式為1.xxx,是以小數點要前移一位(此時指數由-3變成了-2),但這樣尾數就53位了,根據尾數後一位1進0舍的規則,
移動前:
10.0110011001100110011001100110011001100110011001100111
移動後:
1.0011001100110011001100110011001100110011001100110100
将指數去除,最後的小數位值為:
0100110011001100110011001100110011001100110011001101
根據二進制計算規則,小數點後第一位為0.5,第二位為0.25,第三位為0.125....
由于位數太多,我們寫個簡單的計算函數幫助我們計算:
// 二進制小數
var binaryDecimal = '0100110011001100110011001100110011001100110011001101';
// 二進制小數點後對位值
var counterpointArr = [];
// 小數點後二進制的第一個對位值
var counterpoint = 0.5;
for (var i = 0; i < 52; i++) {
counterpointArr.push(counterpoint);
counterpoint = counterpoint / 2;
}
console.log(counterpointArr) // [0.5, 0.25, 0.125, 0.0625, 0.03125, 0.015625, 0.0078...]
let sum = 0;
for (var j = 0; j < binaryDecimal.length; j++) {
if (binaryDecimal.charAt(j) === '1') {
sum = sum + counterpointArr[j]
}
}
console.log(sum) // 0.30000000000000004
相關知識點
特殊場景
如何解決
簡單的普通場景
思路1
通過內插補點跟最小精度比較。
Number.EPSILON是 JavaScript 能夠表示的最小精度。誤差如果小于這個值,就可以認為已經沒有意義了,即不存在誤差了。
function epsEqu(x, y) {
return Math.abs(x - y) < Number.EPSILON; // [ˈepsɪlɒn]
}
console.log(epsEqu(0.1+0.2, 0.3)); // true
console.log(epsEqu(268.34+0.85, 269.19)); // true
思路2
思路就是将小數轉為整數,然後再轉為小數表示。
//注意要傳入兩個小數的字元串表示,不然在小數轉成二進制浮點數的過程中精度就已經損失了
function numAdd(num1/*:String*/, num2/*:String*/) {
var baseNum, baseNum1, baseNum2;
try {
//取得第一個操作數小數點後有幾位數字,注意這裡的num1是字元串形式的
baseNum1 = num1.split(".")[1].length;
} catch (e) {
//沒有小數點就設為0
baseNum1 = 0;
}
try {
//取得第二個操作數小數點後有幾位數字
baseNum2 = num2.split(".")[1].length;
} catch (e) {
baseNum2 = 0;
}
//計算需要 乘上多少數量級 才能把小數轉化為整數
baseNum = Math.pow(10, Math.max(baseNum1, baseNum2));
//把兩個操作數先乘上計算所得數量級轉化為整數再計算,結果再除以這個數量級轉回小數
return (num1 * baseNum + num2 * baseNum) / baseNum;
};
console.log(numAdd('0.1', '0.2')) // 0.3
console.log(numAdd('268.34', '0.85')) // 269.18999999999994
// 其實還是因為num1 * baseNum 導緻的,baseNum為浮點型,先從十進制轉為二進制,二進制時丢失了精度
// 解決方法是 return修改為return (num1 * baseNum + num2 * baseNum).toFixed(0) / baseNum;
// 因為我們已經確定了隻需要整數位
上面的代碼還是比較簡單的,但針對大數、特殊場景都沒做處理,我們可以使用下面的庫
常見庫
1.Math.js
專門為 JavaScript 和 Node.js 提供的一個廣泛的數學庫。支援數字,大數字(超出安全數的數字),複數,分數,機關和矩陣。 功能強大,易于使用。
官網:mathjs.org/
GitHub:github.com/josdejong/m…
2.big.js
官網:mikemcl.github.io/big.js
GitHub:github.com/MikeMcl/big…
扔給後端【推薦】
嘿嘿,後端用的也不是float、double,而是用的bigdecimal。
ps.目前組内針對數值計算一律交給後端的。
感謝那些對我有幫助的文檔:
https://juejin.cn/post/6844903680362151950
https://babbage.cs.qc.cuny.edu/IEEE-754.old/Decimal.html
https://www.bilibili.com/video/BV1i54y1y7Fn?from=search&seid=16329325986965638794
https://www.bilibili.com/read/cv6776011/
https://segmentfault.com/a/1190000009084877