话要从业务代码里的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;记住,包含浮点数的加减乘除均可能致使计算偏差。