在开发中,要进行计算,你可能会遇到小数运算,运气好的话,你的测试测不到精度问题,但其实这是很严重的,如下两个典型例子先感觉如下java
0.1 + 0.2 = 0.30000000000000004
35.41 * 100 = 3540.9999999999995
复制代码
是否是出乎你的意料?安全
写这篇文章的缘由是网上找了些资料,要不就是介绍不全的,要不就是存在错误的(可能你们没发现),要不就是方案还有待增强的。因而我决定本身整理出一份较为全面而不误导别人的文章出来(文章方案对网上大部分资料存在的缺陷进行弥补加强),若是您发现不足,请告诉我,虚心请教。bash
如下咱们了解缘由以及寻找解决方案。ide
在计算机中存储的信息都是二进制来表示,咱们都知道,js中数字类型只有Number
,不像其余语言如java
有int
、double
类型等。它的实现遵循 IEEE 754 标准,使用64位固定长度来表示,也就是标准的 double 双精度浮点数。函数
其中这64位数又分为三部分(从左往右看):测试
以例子0.1 + 0.3
展开说明:ui
0.1
-> 0.0001100110011001100110011001100110011001100110011010
; 0.2
-> 0.0011001100110011001100110011001100110011001100110011
0.0100110011001100110011001100110011001100110011001101
,转化为十进制就是0.30000000000000004
因而就这样,获得了一个出乎你预料的结果了。并非全部小数计算都会这样,是一些小数转化位二进制时出现超出52位数时,就可能会出现这种意外结果。spa
上面咱们知道了出现意外结果是由于小数转化为二进制时发生问题,那整数呢?很天然,整数也是有最大值安全值的,就是2的53次方,为9007199254740992。超出这个数值的计算一样也是带来精度问题。可是咱们通常使用是不会超出这个值的(若是你的需求也要考虑超出这个数值,抱歉,我无能为力)code
若是你的需求能够无视上面整数最大安全值的弊端,那么接下来的解决方案才是适合你的。ip
解放方案:把小数运算中的小数,升级转化为整数(乘以10的n次幂),在进行运算,将最后结果再降级(除以10的n次幂)
上面的描述仅仅是一个思路,一个转化思路。怎么将小数转化为整数,这就讲究了,不能真的进行乘法计算,乘以10的n次幂,还记得前言里的第二个例子吗,这种转化整数的方式自己就是一个小数运算,因此仍是会出现问题。
咱们要用字符串替换的方式来实现这个“升级”:
原值:1.23
转化过程:
1. 化为字符串 '1.23'
2. '1.23'.replace('.', ''),得'123',至关于乘以10的2次幂
复制代码
结合实际例子来了解大概的一个状况,例子1.1 + 1.22
1. 分别对1.1和1.22进行字符串替换,变成'110'和'122',至关于乘以10的2次幂
2. 对替换结果转化回数字类型而后再进行加法运算:110 + 122 = 232
3. 对232除以10的2次幂,得2.32
复制代码
以上就是一个转化和运算过程。
基于上述基本思想,我对加减乘除进行一个方法封装,方便你们进行小数运算。
/** * 带有小数的加法/减法运算 * 减法实际上可当作加法,因此若是要作减法,只需第二个参数即被减数传负值便可 * @param {Number} arg1 - 加数/减数 * @param {Number} arg2 - 加数/被减数 */
function addFloat(arg1, arg2) {
let m = 0; // 记录两个加数中最长的小数位长度
let arg1Str = arg1 + '';
let arg2Str = arg2 + '';
const arg1StrFloat = arg1Str.split('.')[1];
const arg2StrFloat = arg2Str.split('.')[1];
arg1StrFloat && (m = arg1StrFloat.length);
arg2StrFloat && (m = m > arg2StrFloat.length ? m : arg2StrFloat.length);
arg1Str = arg1.toFixed(m); // 主要是为了补零
arg2Str = arg2.toFixed(m);
const transferResult = +(arg1Str.replace('.', '')) + +(arg2Str.replace('.', ''));
return transferResult / Math.pow(10, m);
};
/** * 带有小数的乘法运算 * @param {Number} arg1 - 因数 * @param {Number} arg2 - 因数 */
function multiplyFloat(arg1, arg2) {
let m = 0;
const arg1Str = arg1 + '';
const arg2Str = arg2 + '';
const arg1StrFloat = arg1Str.split('.')[1];
const arg2StrFloat = arg2Str.split('.')[1];
arg1StrFloat && (m += arg1StrFloat.length);
arg2StrFloat && (m += arg2StrFloat.length);
const transferResult = +(arg1Str.replace('.', '')) * +(arg2Str.replace('.', ''));
return transferResult / Math.pow(10, m);;
};
/** * 有小数的除法运算 * @param {Number} arg1 - 除数 * @param {Number} arg2 - 被除数 */
function divideFloat(arg1, arg2) {
const arg1Str = arg1 + '';
const arg2Str = arg2 + '';
const arg1StrFloat = arg1Str.split('.')[1] || '';
const arg2StrFloat = arg2Str.split('.')[1] || '';
const m = arg2StrFloat.length - arg1StrFloat.length;
const transferResult = +(arg1Str.replace('.', '')) / +(arg2Str.replace('.', ''));
return transferResult * Math.pow(10, m);;
};
复制代码
任何一个方案都不能十全十美的,多多少少会有一些限制,毕竟需求是多种多样的。我写文章的习惯就是得告知别人缺陷,而不能忽悠别人。知道本身写的东西的利,也得知道本身的弊。
该方案会有几个小缺陷:
9007199254740992
toFixed
方法,该方法的参数num有个限制:当 num 过小或太大时抛出异常 RangeError。0 ~ 20 之间的值不会引起该异常。以上着两个小缺陷其实在咱们正常开发中,通常不会触及到,由于这样的数字和小数位实在太长了,咱们通常需求不会要求进行这么大的运算以及小数点保留位。
这里顺着这个主题,能够顺带讲一下在js中进行四舍五入或进行小数位保留的状况。
不少人会想到,用tofixed进行四舍五入,实际上,tofixed函数对于四舍五入的规则与数学中的规则不一样,使用的是银行家舍入规则:实际上是一种四舍六入五取偶(又称四舍六入五留双)法。表现为:
四舍六入五考虑,五后非零就进一,五后为零看奇偶,五前为偶应舍去,五前为奇要进一。
很显然,这并非咱们想要的结果。可是Math.round
方法,就是咱们所熟知的四舍五入规则,咱们能够利用该方法扩展到小数位的四舍五入。
网上不少资料都有介绍这种方式:
对小数乘以10的n次幂,再用Math.round取整,再除以10的n次幂,就能获得进过四舍五入后的指定小数位了。
通过上文个人介绍,只要对小数进行数学运算,都有可能出现精度不许确的问题。因此最终的一步正如网上这么多资料说的那样作法,可是其中的乘法和除法,请用文中封装好的方法来进行,而不是直接进行小数的数学运算。
这里我封装一个四舍五入的方法
function roundFloat (value, decimal = 2) {
const n = Math.pow(10, decimal);
return divideFloat(Math.round(multiplyFloat(value, n)), n).toFixed(decimal);
}
复制代码
可能有人会疑问,我这里为何还要用toFixed
,不是说这个不许的吗?
其实我这里并无用toFixed
作实际性上的四舍五入,真正作了四舍五入的工做在调用toFixed
前就已经完成了,最后还用toFixed
只是作润色做用,例如1.1
你要保留小数点后两位的话,理应显示成1.10
,可是js中数字类型显示出来的话,是不会有后面的不起做用的0,所以若是你想显示出指定位数,不足补零的话,就得用toFixed
转化位字符串了。
所以上面的方法的返回结果是一个字符串类型,这你们要注意了,若是你没有这方面的需求,可自行拿掉后面的toFixed
调用。
未经容许,请勿私自转载