学过前端的开发人员在项目开发的时候,都会遇到 0.1+0.2!=0.3 的诡异问题。按照常规的逻辑来思考,这肯定是不符合我们的数学规范。那么JavaScript中为啥会出现这种基本运算错误呢,其中的原理又是什么。这篇文章将从原理给大家梳理此问题的缘由
计算机中的二进制
接下来进入正文,学过计算机基础的人都知道,计算机底层是通过二进制来进行数据之间的交互的。其中我们应该要明白为什么计算机通过二进制来进行数据交互,以及二进制是什么
- 计算机为什么要通过二进制来进行数据交互?
在我们日常使用的电子计算机中,数字电路组成了我们计算机物理基础构成,这些数字电路可以看成是一个个门电路集合组成,门电路的理论基础是逻辑运算。那么当我们的计算机的电路通电工作,每个输出端就有了电压。电压的高低通过模数转换即转换成了二进制:高电平是由1表示,低电平由0表示。
说得简单点,就是计算机的基本运行是由电路支持的,电路容易识别高低电压,即电路只要能识别低、高就可以表示“0”和“1”。
- 二进制是什么
21:十进制整数转二进制
21 / 2 = 10 -- 1 ⬆
10 / 2 = 5 -- 0 ⬆
5 / 2 = 2 -- 1 ⬆
2 / 2 = 1 -- 0 ⬆
1 / 2 = 0 -- 1 ⬆
二进制(反取余数):10101
十进制小数转换为二进制
将0.125换算为二进制
0.125 * 2 = 0.25 -- 0 ⬇
0.25 * 2 = 0.5 -- 0 ⬇
0.5 * 2 = 1.0 -- 1 ⬇
二进制:0.001
二进制转化为十进制
二进制转化为十进制,整数部分和小数部分的方法都是相同的。方法:将二进制每位上的数乘以权,然后相加之和即是十进制数
回到JavaScript
- JavaScript规范中的数值量,为什么是这个数?
首先需明白在JavaScript中的数字是64-bits的双精度,所以有264种可能性,在上述中提到,当E全为1的时候,表示的要么为无穷数,要么为NaN。所以不是数值的可能为253种,同时JavaScript中把+∞和-∞、NaN定义为数值。所以JavaScript数值的总量为
- JavaScript中的最大安全整数值为什么为9007199254740991
上述提及,有效数字有53个(包括最前面一位的1.xxxx中的1),如果超出了小数点后面52位以外的话,就遵从二进制舍0进1的原则,那么这样的数字就不是一一对应的,会有误差,精度就丢失了。也就不是安全数了。所以JavaScript中的最大安全整数值为
- 0.1 + 0.2 != 0.3?
这个问题也许是大家最关心的问题,也是最经典的JavaScript面试问题。不过学习了上面的知识之后,大家已经明白了问题产生的原因(精度丢失),那么具体是如何丢失的呢?
首先,0.1 + 0.2 这个运算是十进制的加法,上述提及,计算机处理十进制的加法其实是先将十进制转化为二进制之后再运算处理。那么我们需要计算出0.1的二进制、0.2的二进制以及0.3的二进制来进行对比校验。
根据上述的计算方法,我们很容易得出0.1的二进制是无限循环的,即
0.1D = (-1)^0 * 1.1001..(1001循环13次)1010B * 2^-4
0.2D = (-1)^0 * 1.1001..(1001循环13次)1010B * 2^-3
0.3D = (-1)^0 * 1.0011..(0011循环13次)0011B * 2^-2
可以看出,当0.1,0.2转化为二进制的时候,有效数字都是52位(4 * 12 + 4),因为在64位精确度中,只能保持52位有效数字,如果没有52位有效数字的约束,其实在第53位中,0.1转二进制本来是1,但是有了52位约束之后,根据二进制的取舍
,最后五位数就从1001 1(第53位) 变成了 1010。
我们可以手动计算一下0.1的二进制加上0.2的二进制
那么相加结果转换为十进制其实等于0.30000000000000004,这就是为什么0.1 + 0.2 != 0.3 的原因了。
附上js源码计算
//除法函数,用来得到精确的除法结果
//说明:javascript的除法结果会有误差,在两个浮点数相除的时候会比较明显。这个函数返回较为精确的除法结果。
//调用:accDiv(arg1,arg2)
//返回值:arg1除以arg2的精确结果
function accDiv(arg1, arg2) {
var t1 = 0,
t2 = 0,
r1, r2;
try {
t1 = arg1.toString().split(".")[1].length
} catch (e) {}
try {
t2 = arg2.toString().split(".")[1].length
} catch (e) {}
r1 = Number(arg1.toString().replace(".", ""));
r2 = Number(arg2.toString().replace(".", ""));
if (r2 == 0) {
return Infinity;
} else {
return (r1 / r2) * Math.pow(10, t2 - t1);
}
}
//乘法函数,用来得到精确的乘法结果
//说明:javascript的乘法结果会有误差,在两个浮点数相乘的时候会比较明显。这个函数返回较为精确的乘法结果。
//调用:accMul(arg1,arg2)
//返回值:arg1乘以arg2的精确结果
function accMul(arg1, arg2) {
var m = 0,
s1 = arg1.toString(),
s2 = arg2.toString();
try {
m += s1.split(".")[1].length
} catch (e) {}
try {
m += s2.split(".")[1].length
} catch (e) {}
return Number(s1.replace(".", "")) * Number(s2.replace(".", "")) / Math.pow(10, m);
}
//加法函数,用来得到精确的加法结果
//说明:javascript的加法结果会有误差,在两个浮点数相加的时候会比较明显。这个函数返回较为精确的加法结果。
//调用:accAdd(arg1,arg2)
//返回值:arg1加上arg2的精确结果
function accAdd(arg1, arg2) {
var r1, r2, m;
try {
r1 = arg1.toString().split(".")[1].length;
} catch (e) {
r1 = 0;
}
try {
r2 = arg2.toString().split(".")[1].length;
} catch (e) {
r2 = 0;
}
m = Math.pow(10, Math.max(r1, r2));
return (arg1 * m + arg2 * m) / m;
}
//减法函数
function accSub(arg1, arg2) {
var r1, r2, m, n;
try {
r1 = arg1.toString().split(".")[1].length;
} catch (e) {
r1 = 0;
}
try {
r2 = arg2.toString().split(".")[1].length;
} catch (e) {
r2 = 0;
}
m = Math.pow(10, Math.max(r1, r2));
//last modify by deeka
//动态控制精度长度
n = (r1 >= r2) ? r1 : r2;
return ((arg2 * m - arg1 * m) / m).toFixed(n);
}
//给Number类型增加一个add方法,调用起来更加方便。
Number.prototype.add = function (arg) {
return accAdd(arg, this);
};
//给Number类增加一个sub方法,调用起来更加方便
Number.prototype.sub = function (arg) {
return accSub(arg, this);
};
//给Number类型增加一个mul方法
Number.prototype.mul = function (arg) {
return accMul(arg, this);
};
//给Number类型扩展一个div方法
Number.prototype.div = function (arg) {
return accDiv(this, arg);
};
export {
accDiv,
accMul,
accAdd,
accSub,
}
npm库
number-precision
import NP from 'number-precision'
NP.strip(0.09999999999999998); // = 0.1
NP.plus(0.1, 0.2); // = 0.3, not 0.30000000000000004
NP.plus(2.3, 2.4); // = 4.7, not 4.699999999999999
NP.minus(1.0, 0.9); // = 0.1, not 0.09999999999999998
NP.times(3, 0.3); // = 0.9, not 0.8999999999999999
NP.times(0.362, 100); // = 36.2, not 36.199999999999996
NP.divide(1.21, 1.1); // = 1.1, not 1.0999999999999999
NP.round(0.105, 2); // = 0.11, not 0.1
参考:https://www.cnblogs.com/yugueilou/p/14629506.html
https://juejin.cn/post/6844903763870744590