JS浮点运算精度问题
一、问题描述
在JS中,整数和浮点数都是Number数据类型,所有的数字都是以64位浮点数的形式存储的,即便是整数也是如此。所以我们在打印 1.00 的时候,显示的却是 1。当浮点数作为数学运算的时候,也会经常遇到一些奇怪的问题。比如下面:
// 加法运算 0.1 + 0.2 = 0.30000000000000004 0.7 + 0.1 = 0.7999999999999999 0.2 + 0.4 = 0.6000000000000001 2.22 + 0.1 = 2.3200000000000003 // 减法运算 1.5 - 1.2 = 0.30000000000000004 0.3 - 0.2 = 0.09999999999999998 // 乘法运算 19.9 * 100 = 1989.9999999999998 0.7 * 180 = 125.99999999999999 0.55 * 100 = 55.00000000000001 // 除法运算 0.3 / 0.1 = 2.9999999999999996 0.69 / 10 = 0.06899999999999999
这些问题常常会困扰我们的计算结果。出现很多意想不到的bug。那么问题的原因是什么呢?
二、问题原因
看了上面的结果,似乎有点不可思议,这种小学生都不会算错的题。强大的计算机怎么会出现错误呢。我们来看下具体的原因。
在js里,数字都是采用 IEEE 745标准 的64位双精度浮点数进行存储的,该规范定义了浮点数的格式,对于64位的浮点数,在内存中最高的1位是符号位,接着的11位是指数,剩下的52位为有效数字:
- 第0位:符号位,s表示,0表示整数,1表示负数
- 第1位到第11位:存储指数部分,用 e 表示
- 第12位到第63位:存储小数部分,用 f 表示
符号位决定了一个数的正负,指数部分决定了数值的大小,小数部分决定了数值的精度。 IEEE 754规定,有效数字第一位默认总是1,不保存在64位浮点数之中。也就是说,有效数字总是1.xx…xx的形式,其中xx..xx的部分保存在64位浮点数之中,最长可能为52位。因此,JavaScript提供的有效数字最长为53个二进制位(64位浮点的后52位+有效数字第一位的1)。
三、计算过程
在JS中,计算 0.1 + 0.2 到底是个什么过程呢?
首先,十进制的 0.1 和 0.2 都会被转为二进制,但是浮点数使用二进制表达时是无穷的,比如:
0.1 -> 0.0001100110011001...(无限)
0.2 -> 0.0011001100110011...(无限)
由于IEEE745标准的64位双精度浮点数的小数部分最多支持 53 位二进制,所以两者相加之后得到的二进制为
0.0100110011001100110011001100110011001100110011001100
因此再将上面的二进制转为十进制,就成了0.30000000000000004,所以浮点数在进行算术运算时会产生误差。
四、整数的精度问题
在JS中整数也同样存在精度问题,比如下面的代码
console.log(19571992547450991); //=> 19571992547450990 console.log(19571992547450991===19571992547450992); //=> true
原因和上面也相同,在js中,Number类型统一安装浮点数处理,整数是按照最大54位来算。
最大:2^53 - 1, Number.MAX_SAFE_INTEGER, 9007199254740991
最小:-(2^53 - 1),Number.MIN_SAFE_INTEGER,-9007199254740991
在这个范围之类的数,叫安全整数,所以超过这个方位,就会存在被社区的精度问题。
当然以上问题并不只是在JS中才会出现,几乎所有采用IEEE745标准的编程语言都会有精度问题,只不过其他很多语言都已经封装了方法来避免精度问题,而JS是一门弱类型的语言,从设计思想上就没有对浮点数有严格的数据类型,所以精度误差问题就会经常遇到。
五、解决方案
1. 类库
很多对精度要求较高的计算,都应该交给后端去计算和存储,因为后端有成熟的库来解决这种计算精度问题。当然前端也有一些不错的库:
Math.js
Math.js是专门为JS和node.js提供的一个数学库,它具有灵活的表达解析器,支持符号计算,配置有大量的函数和常量,比如大数计算,复数,分数,单位和矩阵,功能强大,易于使用
gitHub: https://github.com/josdejong/mathjs
decimal.js
为JS提供十进制类型的任意精度的计算
gitHub: https://github.com/MikeMcl/decimal.js
big.js
gitHub: https://github.com/MikeMcl/big.js/
以上这些类库能够帮助我们解决很多问题,不过我们前端通常只做一些简单的加减乘除运算,使用库函数就显得有点多余,一个函数就能解决
2. 整数表示
对于整数,大数字可以采用字符串的形式进行表示
3. 格式化数字,金额,保留几位小数等
参考:https://www.html.cn/archives/7324
4. 浮点数运算
浮点运算的解决方案有很多,最常用的方法就是在判断浮点数运算结果钱对其结果进行精度缩小。
也可以采用一些网上封装好的函数进行处理:
加法运算
/** ** 加法函数,用来得到精确的加法结果 ** 说明:javascript的加法结果会有误差,在两个浮点数相加的时候会比较明显。这个函数返回较为精确的加法结果。 ** 调用:accAdd(arg1,arg2) ** 返回值:arg1加上arg2的精确结果 **/ function accAdd (arg1, arg2) { var r1, r2, m, c; try { r1 = arg1.toString().split('.')[1].length; } catch (e) { r1 = 0; } try { r2 = arg2.toString().split('.')[1].length; } catch (e) { r2 = 0; } c = Math.abs(r1 - r2); m = Math.pow(10, Math.max(r1, r2)); if (c > 0) { var cm = Math.pow(10, c); if (r1 > r2) { arg1 = Number(arg1.toString().replace('.', '')); arg2 = Number(arg2.toString().replace('.', '')) * cm; } else { arg1 = Number(arg1.toString().replace('.', '')) * cm; arg2 = Number(arg2.toString().replace('.', '')); } } else { arg1 = Number(arg1.toString().replace('.', '')); arg2 = Number(arg2.toString().replace('.', '')); } return (arg1 + arg2) / m; } // 给Number类型增加一个add方法,调用起来更加方便。 Number.prototype.add = function (arg) { return accAdd(arg, this); };
减法运算
/** ** 减法函数,用来得到精确的减法结果 ** 说明:javascript的减法结果会有误差,在两个浮点数相减的时候会比较明显。这个函数返回较为精确的减法结果。 ** 调用:accSub(arg1,arg2) ** 返回值:arg1加上arg2的精确结果 **/ 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 ((arg1 * m - arg2 * m) / m).toFixed(n); } // 给Number类型增加一个mul方法,调用起来更加方便。 Number.prototype.sub = function (arg) { return accMul(arg, this); };
乘法运算
/** ** 乘法函数,用来得到精确的乘法结果 ** 说明:javascript的乘法结果会有误差,在两个浮点数相乘的时候会比较明显。这个函数返回较为精确的乘法结果。 ** 调用:accMul(arg1,arg2) ** 返回值:arg1乘以 arg2的精确结果 **/ function accMul (arg1, arg2) { var m = 0; var s1 = arg1.toString(); var 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); } // 给Number类型增加一个mul方法,调用起来更加方便。 Number.prototype.mul = function (arg) { return accMul(arg, this); };
除法运算
/** ** 除法函数,用来得到精确的除法结果 ** 说明:javascript的除法结果会有误差,在两个浮点数相除的时候会比较明显。这个函数返回较为精确的除法结果。 ** 调用:accDiv(arg1,arg2) ** 返回值:arg1除以arg2的精确结果 **/ function accDiv (arg1, arg2) { var t1 = 0; var t2 = 0; var 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('.', '')); return (r1 / r2) * Math.pow(10, t2 - t1); } // 给Number类型增加一个div方法,调用起来更加方便。 Number.prototype.div = function (arg) { return accDiv(this, arg); };
参考:
https://juejin.im/post/6844903572979597319
https://www.html.cn/archives/7340