小数精度丢失

来自:javascript小数精度丢失

一、为什么小数精度会丢失

Javascript采用了IEEE-745浮点数表示法(几乎所有的编程语言都采用),这是一种二进制表示法,可以精确地表示分数,比如1/2,1/8,1/1024。遗憾的是,我们常用的分数(特别是在金融的计算方面)都是十进制分数1/10,1/100等。二进制浮点数表示法并不能精确的表示类似0.1这样 的简单的数字,上诉代码的中的x和y的值非常接近最终的正确值,这种计算结果可以胜任大多数的计算任务:这个问题也只有在比较两个值是否相等时才会出现。
这个问题并不是只在javascript中才会出现,在任何使用二进制浮点数的编程语言中都会出现这个问题。 所以说,精度丢失并不是语言的问题,而是浮点数存储本身固有的缺陷。只不过在 C++/C#/Java 这些语言中已经封装好了方法来避免精度的问题,而 JavaScript 是一门弱类型的语言,从设计思想上就没有对浮点数有个严格的数据类型,所以精度误差的问题就显得格外突出。

javascript的未来版本或许会支持十进制数字类型以避免这些舍入问题,在这之前,你更愿意使用大整数进行重要的金融计算,例如,要使用整数‘分’而不是使用小数‘元’进行货比单位的运算。

二、如何避免精度丢失

一般常用的有四个方法,第一个是设置一个“能够接受的误差范围”,在这个范围内,可认为没有误差;第二个是使用三方的类库math.js;第三是使用toFixed()方法;第四是封装一个计算类(加、减、乘、除)。

1、ES6在Number对象上面,新增了一个极小的常量Number.EPSILON,它表示1与大于1的最小浮点数之间的差,它是实际上是javascript能够表示的最小精度(可以接受的最小误差范围),误差如果小于这个值,就可以认为已经没有意义了,即不存在误差。

1
2
3
4
5
6
7
//Number.EPSILON等于2^-52
console.log(Number.EPSILON === Math.pow(2, -52)); //true
function withinErrorMargin (left, right) {  
return Math.abs(left - right) < Number.EPSILON * Math.pow(2, 2);
}
withinErrorMargin(0.1 + 0.2, 0.3) //true

2、math.js是一个广泛应用于JavaScript 和 Node.js的数学库,它的特点是灵活表达式解析器,支持符号计算,内置大量函数与常量,并提供集成解决方案来处理不同的数据类型,如数字,大数字,复数,分数,单位和矩阵。

3、定义:toFixed() 方法可把 Number 四舍五入为指定小数位数的数字。
  用法:NumberObject.toFixed(num) 其中num是必须的,规定小数的位数,是 0 ~ 20 之间的值,包括 0 和 20,有些实现可以支持更大的数值范围。如果省略了该参数,将用 0 代替。
然而实际上,并不是完美的,可能你开发时候测试的几个实例恰巧都是你想要的结果,可能在实际上线后遇到大量的数据后发现出问题了,不能正确的计算。一般是在遇到最后一位是5的时候,就不是’四舍五入”,eg:2.55.toFixed(1) // 2.5,而我们齐期望的是2.56。
我有查这个产生误差的原因,有人说是“银行家的舍入规则”,即四舍六入五考虑,这里“四”是指≤4 时舍去,”六”是指≥6时进上。”五”指的是根据5后面的数字来定,当5后有数时,舍5入1;当5后无有效数字时,需要分两种情况来讲:5前为奇数,舍5入1;5前为偶数,舍5不进(0是偶数)。但在某些情况下也是不成立。

1
2
3
2.65.toFixed(1) //2.6 结果正确
2.45.toFixed(1) //2.5 希望得到的结果是2.4
2.35.toFixed(1) //2.4 结果正确

由于无法解决这种问题,所以看到有一些是以项目需求为准重写符合要求的函数,在Math.round(x)来扩展解决toFixed()四舍五入不精确的问题。 
原本round(x) 方法可把一个数字四舍五入为最接近的整数,其中x是必须的且必须是数字。虽然解决了四舍五入的问题,但却没有直接解决保留小数点后多少位的问题,因而需要重写符合需求的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function RoundNum(n, m){ //n表示需要四舍五入的数,m表示需要保留的小数位数
var newNum = Math.round(n * Math.pow(10, m)) / Math.pow(10, m) ;
//首先将要保留的小数位数的小数部分转成整数部分,利用幂函数将n乘以10的m次方
//然后利用Math.round()方法进行四舍五入处理
//最后再除以10的m次方还原小数部分
//注:此时还未能将所有数字正确转换。例如将1.0001保留3位小数我们想要的结果是1.000,而此时newNum里面的值是1
//所以还需要处理此种特殊情况,即保留的小数位上全0
var newSNum = newNum.toString();
//这一步将刚才进行处理过的数转换成字符串
var rs = newSNum.indexOf('.'); //利用indexOf查找字符串中是否有.,它返回某个指定的字符串值在字符串中首次出现的位置,不存在则返回-1
if (rs < 0) {
rs = newSNum.length;
newSNum += '.';
}
while (newSNum.length <= rs + m) { //在末尾加0
newSNum += '0';
}
return newSNum;
}
console.log(RoundNum(1.0005, 3)); //得到1.001

toFixed的修复

1
2
3
4
5
6
function toFixed(num, s) {
var times = Math.pow(10, s)
var des = num * times + 0.5
des = parseInt(des, 10) / times
return des + ''
}

4、封装一个计算类

三、javascript可以存储的 最大数字以及最大安全数字

最大数字是Number.MAX_VALUE、最大安全数字是Number.MAX_SAFE_INTEGER。Number.MAX_VALUE大于Number.MAX_SAFE_INTEGER,我的理解是js可以精确表示最大安全数字以内的数,超过了最大安全数字但没超过最大数字可以表示,但不精确,如果超过了最大数字,则这个数值会自动转换成特殊的Infinity值。

由于内存的限制,ECMAScript并不能保存世界上所有的数值,ECMAScript能够表示的最小数值是Number.MIN_VALUE,能够表示的最大数值是Number.MAX_VALUE。超过数值是正值,则被转成Infinity(正无穷),如果是负值则被转成-Infinity(负无穷)。如果在某次返回了正或负的Infinity值,那么该值将无法继续参与下一次的计算,所以我们需要确定一个数值是不是有穷的,即是不是位于最小和最大的数值之间,可以使用isFinite()函数,如果该函数参数在最小和最大数值之间时会返回true。注意,如果参数类型不是数值,Number.isFinite一律返回false。

JavaScript 能够准确表示的整数范围在-2^53到2^53之间(不含两个端点),超过这个范围,无法精确表示这个值。ES6 引入了Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限。Number.isSafeInteger()则是用来判断一个整数是否落在这个范围之内。