話要從業務代碼裏的bug提及,大體過程是前端運算 2.07-1 以後結果倒是1.0699999999999998,老司機們都知道是浮點數運算的精度丟失致使的,在查看了下具體代碼,果真處理不當。所以我深究一番,並誕生了此文。此處重點強調兩個認識誤區:javascript
- 浮點數運算精度丟失問題並非js獨有的!
- js浮點數的加減乘除運算均可能致使精度丟失問題!
首先不得不說說浮點數的表示方法,任何數在計算機面前都會被處理成二進制,而數字的二進制表示主要有原碼、反碼、補碼。(有點熟悉對不對?哥就是來給你補計算機組成原理的,壞笑~)前端
原碼是計算機中對數字的二進制的定點表示方法,最高位表示符號位,其他位表示數值位。優勢顯而易見,簡單直觀;缺點也很明顯,不能直接參與運算,可能會報錯,如11+(-11) => 10010110 => -22,結果居然不等於0。(臥槽,瞎搞啊~,覺得我沒上過學?)因此,原碼符號位不能直接參與運算。說到這,給你們個思考題,8位有符號的原碼錶示範圍是多少?本身思考哈~java
正數的反碼和其原碼同樣;負數的反碼,符號位爲1,數值部分按原碼取反。例如 [+7]原 = 00000111,[+7]反 = 00000111; [-7]原 = 10000111,[-7]反 = 11111000。ui
正數的補碼和其原碼同樣;負數的補碼爲其反碼加1。例如 [+7]原 = 00000111,[+7]反 = 00000111,[+7]補 = 00000111; [-7]原 = 10000111,[-7]反 = 11111000,[-7]補 = 11111001。
說到這,你也許會問,哥你這都是講的整數啊,沒說到浮點數啊。別急,弟繼續往下看~spa
國際標準IEEE 754規定,任意一個二進制浮點數V均可以表示成下列形式:設計
舉個小栗子🌰:
-0.5 => -0.1[二進制]
=> -1.0 * 2^-1
=> (-1)^1 * 1.0 * 2^-1
=> s=1,M=1.0,E=-13d
IEEE 754又規定了,浮點數分單精度雙精度之分:code
對於有效數字M和指數E,這個IEEE 754還規定了:cdn
有人又要問了,哥,爲啥子要有中間數?本身思考哈,弟你本身要學會成長,實在不行你也能夠問你谷哥~blog
Attention! 精華部分來了~
浮點數的加法運算(不要問哥爲啥只講加法~)分爲下面幾個步驟:
(1)對階
顧名思義就是對齊階碼,使兩數的小數點位置對齊,小階向大階對齊;
(2)尾數求和
對階完對尾數求和
(3)規格化
尾數必須規格化成1.M的形式
(4)舍入
在規格化時會損失精度,因此用舍入來提升精度,經常使用的有0舍1入法,置1法
(5)校驗判斷
最後一步是校驗結果是否溢出。若階碼上溢則置爲溢出,下溢則置爲機器零
0.2 => 1/8 + 1/16 + 1/128 +... => 1.100110011001100...*2^-3 =>
⚠️ 最後的0被移出去了,這就是偏差產生的根源!
(4)舍入
(5)校驗判斷
0.2 + 0.4 => 0 01111110 (1)00110011001100110011001 => 1.1999999285/2 => 0.5999999643 (並不等於0.6)
最後發現計算結果果真出現偏差,由於在尾數規格化的步驟中可能產生移位偏差,看來要想精確運算,不能直接操做浮點數運算啊!最保險的方法是在運算過程當中,將浮點數處理成整數進行運算:
/** * [scaleNum 經過操做其字符串將一個浮點數放大或縮小] * @param {number} num 要放縮的浮點數 * @param {number} pos 小數點移動位數 * pos大於0爲放大,小於0爲縮小;不傳則默認將其變成整數 * @return {number} 放縮後的數 */
function scaleNum(num, pos) {
if (num === 0 || pos === 0) {
return num;
}
let parts = num.toString().split('.');
const intLen = parts[0].length;
const decimalLen = parts[1] ? parts[1].length : 0;
// 默認將其變成整數,放大倍數爲原來小數位數
if (pos === undefined) {
return parseFloat(parts[0] + parts[1]);
} else if (pos > 0) {
// 放大
let zeros = pos - decimalLen;
while (zeros > 0) {
zeros -= 1;
parts.push(0);
}
} else {
// 縮小
let zeros = Math.abs(pos) - intLen;
while (zeros > 0) {
zeros -= 1;
parts.unshift(0);
}
}
const idx = intLen + pos;
parts = parts.join('').split('');
parts.splice(idx > 0 ? idx : 0, 0, '.');
return parseFloat(parts.join(''));
}複製代碼
有不少同窗將浮點數擴大成整數,直接乘以10^N,其實這也會可能致使偏差,例如 0.57*100 => 56.99999999999999;另外除法運算也可能致使偏差,5.7/10 => 0.5700000000000001;記住,包含浮點數的加減乘除均可能致使計算偏差。