浮点数精度丢失,一直是前端面试八股文里很常见的一个问题,今天我们就来深入的了解一下问题背后的原理,以及给一些日常处理的小技巧。
1、0.1 + 0.2 ≠ 0.3反而 0.1+0.2 = 0.30000000000000004
2、2.55.toFixed(1) = 2.5, 而 1.55.toFixed(1) = 1.6
其实以上都是因为浮点数精度问题导致的
我们在计算数学问题的时候,使用的是十进制,计算0.1 + 0.2的结果等于0.3,没有任何问题。但在计算机中,存储数据使用的是二进制,数据由0和1组成。所以在对数据进行计算时,需要将数据全部转换成二进制,再进行数据计算。
十进制转二进制的主要原则如下所示:
除二取余,然后倒序排列,高位补零
例如: 65 转二进制
65 转二进制为 1000001,高位 0 后为 01000001
对小数点以后的数乘以 2,取整数部分,再取小数部分乘 2,以此类推…… 直到小数部分为 0 或位数足够。取整部分按先后顺序排列即可
例如: 0.1 转二进制
0.1 * 2 = 0.2 --------------- 取整数 0,小数 0.2
0.2 * 2 = 0.4 --------------- 取整数 0,小数 0.4
0.4 * 2 = 0.8 --------------- 取整数 0,小数 0.8
0.8 * 2 = 1.6 --------------- 取整数 1,小数 0.6
0.6 * 2 = 1.2 --------------- 取整数 1,小数 0.2
0.2 * 2 = 0.4 --------------- 取整数 0,小数 0.4
0.4 * 2 = 0.8 --------------- 取整数 0,小数 0.8
0.8 * 2 = 1.6 --------------- 取整数 1,小数 0.6
0.6 * 2 = 1.2 --------------- 取整数 1,小数 0.2
...
最终 0.1 的二进制表示为 0.000110011… 后面将会 0011 无限循环,因此二进制无法精确的保存类似 0.1 这样的小数。十进制小数转二进制后大概率出现无限位数!但计算机存储是有限,那这样无限循环也不是办法,又该保存多少位呢?也就有了我们接下来要重点讲解的 IEEE 754 标准。
维基百科链接 的链接,感兴趣的自行了解一下。
这里用一句话概述,IEEE754是一种二进制浮点数算术标准。
IEEE 754 常用的两种浮点数值的表示方式为:单精确度(32位)、双精确度(64位)。例如, java语言中的 float 通常是指 IEEE 单精确度,而 double 是指双精确度。
在 JavaScript 中不论小数还是整数只有一种数据类型表示,这就是 Number 类型,其遵循 IEEE 754 标准,使用双精度浮点数(double)64 位(8 字节)来存储一个浮点数。
浮点型数据类型主要有:单精度(float)、双精度(double)
在内存中占 4 个字节、有效数字 8 位、表示范围:-3.40E+38 ~ +3.40E+38
在内存中占 8 个字节、有效数字 16 位、表示范围:-1.79E+308 ~ +1.79E+308
在这个标准下,我们会用 1 位存储 S(sign),0 表示正数,1 表示负数。用 11 位存储 E(exponent) + bias,对于 11 位来说,bias 的值是 2^(11-1) - 1,也就是 1023。用 52 位存储 Fraction。
举个例子,就拿 0.1 来看,对应二进制是 1 * 1.1001100110011…… * 2^-4, Sign 是 0,E + bias 是 -4 + 1023 = 1019,1019 用二进制表示是 1111111011,Fraction 是 1001100110011……
0.1 用64为表示
0 01111111011 1001100110011001100110011001100110011001100110011010
0.2 用64为表示
0 01111111100 1001100110011001100110011001100110011001100110011010
可以看出来在转换为二进制时
0.1 >>> 0.0001 1001 1001 1001...(1001无限循环)
0.2 >>> 0.0011 0011 0011 0011...(0011无限循环)
将 0.1 和 0.2 的二进制形式按实际展开,末尾补零相加,结果如下:
0.00011001100110011001100110011001100110011001100110011010
+0.00110011001100110011001100110011001100110011001100110100
=0.01001100110011001100110011001100110011001100110011001110
用科学计数法表示为:
1.001100110011001100110011001100110011001100110011010 * 2^(-2)
因此 0.1 + 0.2
0 01111111101 0011001100110011001100110011001100110011001100110100
再转十进制为:0.30000000000000004
小结:计算机存储双进度浮点数,需要先把十进制转换为二进制的科学计数法形式,然后计算机以一定的规则(IEEE 754)存储,因为存储时有位数限制(双进度 8 字节,64 位),末位就需要取近似值(0 舍 1 入),再转换为十进制时,就造成了误差。
function add(num1, num2) {
//num1 小数位的长度
const num1Digits = (num1.toString().split('.')[1] || '').length;
//num2 小数位的长度
const num2Digits = (num2.toString().split('.')[1] || '').length;
// 取最大的小数位作为10的指数
const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits));
// 把小数都转为整数然后再计算
return (num1 * baseNum + num2 * baseNum) / baseNum;
}
把计算数字 提升 10 的N次方 倍 再 除以 10的N次方。N>1
function numbersequal(a,b){
return Math.abs(a-b) < Number.EPSILON;
}
var a=0.1+0.2, b=0.3;
console.log(numbersequal(a,b)); //true
1、math.js
math.js是JavaScript和Node.js的一个广泛的数学库。支持数字,大数,复数,分数,单位和矩阵等数据类型的运算。
官网:http://mathjs.org/
GitHub:https://github.com/josdejong/mathjs
0.1+0.2 ===0.3实现代码:
var math = require('mathjs')
console.log(math.add(0.1,0.2))//0.30000000000000004
console.log(math.format((math.add(math.bignumber(0.1),math.bignumber(0.2)))))//'0.3'
2、decimal.js
为 JavaScript 提供十进制类型的任意精度数值。
官网:http://mikemcl.github.io/decimal.js/
GitHub:https://github.com/MikeMcl/decimal.js
var Decimal = require('decimal.js')
x = new Decimal(0.1)
y = 0.2
console.log(x.plus(y).toString())//'0.3'
3、bignumber.js
用于任意精度算术的JavaScript库。
官网:[http://mikemcl.github.io/bignumber.js/](http://mikemcl.github.io/bignumber.js/)
Github:https://github.com/MikeMcl/bignumber.js
var BigNumber = require("bignumber.js")
x = new BigNumber(0.1)
y = 0.2
console.log(x.plus(y).toString())//'0.3'
4、big.js
用于任意精度十进制算术的小型快速JavaScript库。
官网:http://mikemcl.github.io/big.js/
Github:https://github.com/MikeMcl/big.js/
var Big = require("big.js")
x = new Big(0.1)
y = 0.2
console.log(x.plus(y).toString())//'0.3'
javascript中toFixed使用的是银行家舍入规则。
据说大部分的编程软件都使用的是这种方法,也算是一种国际标准。所谓银行家舍入法,其实质是一种四舍六入五取偶(又称四舍六入五留双)法。其规则是:当舍去位的数值小于5时,直接舍去该位;当舍去位的数值大于等于6时,在舍去该位的同时向前位进一;当舍去位的数值等于5时,如果前位数值为奇,则在舍去该位的同时向前位进一,如果前位数值为偶,则直接舍去该位。
网上给了一种通用的解法,在四舍五入前,给数字加一个极小值,比如 1e-14:
这样处理后,大部分场景下的精度基本都够用了。
这里我们采用的极小值是 10 的负 14 次方(1e-14),有没有一个官方推荐的极小值常量呢?嘿,巧了,还真有!ES6 在 Number 对象上新增了一个极小的常量 Number.EPSILON:
Number.EPSILON
// 2.220446049250313e-16
Number.EPSILON.toFixed(20)
// "0.00000000000000022204"
引入一个这么小的量,目的在于为浮点数计算设置一个误差范围,如果误差能够小于 Number.EPSILON,我们就可以认为结果是可靠的。
参考链接:
https://www.jianshu.com/p/f5e106affd96