javascript的精度问题,包含原因和解决方案

您所在的位置:网站首页 js计算金额 javascript的精度问题,包含原因和解决方案

javascript的精度问题,包含原因和解决方案

2023-07-07 05:44| 来源: 网络整理| 查看: 265

前言

    作为一个高级前端程序员,数据精度问题一定不能出错。那么我们对于js精度问题,应该了解一些什么呢?

 js浮点数运算为什么出现精度失真?

解决的方法有哪些? 

想要解决问题,我们先来弄清楚问题产生的根本原因

原因

    知道原因必须了解,js浮点数运算标准和规则。javascript的浮点数运算就是采用了IEEE 754的标准。

IEEE 754

    IEEE二进制浮点数算术标准IEEE 754是20世纪80年代以来最广泛使用的浮点数运算标准。

     IEEE 754规定了四种表示浮点数值的方式:单精确度(32位)、双精确度(64位)、延伸单精确度(43比特以上,很少使用)与延伸双精确度(79比特以上,通常以80位实现)。其中javascript采用的是 双精度(64位)浮点运算规则。

    

IEEE754存储和运算规则是怎么样的呢?

一个浮点数在计算机中表示为:

Value = sign x exponent x function

也就是浮点数的实际值等于符号位(sign)乘以指数偏移值(exponent)再乘以分数值。

sign:最高有效位被指定为正负的符号位, 0表示正数,1表示负数

exponent:表示指数偏移值,等于指数值加上某个固定的值。固定值为:2^e - 1,其中e为存储指数的长度,比如32位的是8,64位的为11

fraction: 为尾数,可以理解为小数点部分。超出的部分自动进一舍零。

S为符号位,Exp为指数字,Fraction为有效数字。指数部分即使用所谓的偏正值形式表示,偏正值为实际的指数大小与一个固定值(64位的情况是1023)的和。采用这种方式表示的目的是简化比较。因为,指数的值可能为正也可能为负,如果采用补码表示的话,全体符号位S和Exp自身的符号位将导致不能简单的进行大小比较。正因为如此,指数部分通常采用一个无符号的正数值存储。双精度的指数部分是−1022~+1023加上1023,指数值的大小从1~2046(0(2进位全为0)和2047(2进位全为1)是特殊值)。浮点小数计算时,指数值减去偏正值将是实际的指数大小。

数符阶码尾数32位182364位11152

我们来个例子🌰,把10进制数转化位IEEE754 的32位浮点数表示。

例如: 178.125

第一步:将整数部分和小数部分转化为二进制

整数部分用除2取余的方法,求得:10110010 小数部分用乘2取整的方法,求得:001 合起来为: 10110010.001

第二步:转化为二进制浮点数。

    即把小数点移动到只有一位整数:1.0110010001 * 2^111。左移了7位二进制为111。

第三步:确定符号位,阶码,尾数

   数符:由于浮点数是正数,故为0.(负数为1)

阶码 : 阶码的计算公式:阶数 + 偏移量, 阶码是需要作移码运算,在转换出来的二进制数里,阶数是111(十进制为7),对于单精度的浮点数,偏移值为01111111(127)[偏移量的计算是:2^(e-1)-1, e为阶码的位数,即为8,因此偏移值是127],即:111+01111111 = 10000110

尾数:小数点后面的数,即0110010001 

    

可能有个疑问:小数点前面的1去哪里了?由于尾数部分是规格化表示的,最高位总是“1”,所以这是直接隐藏掉,同时也节省了1个位出来存储小数,提高精度。

浮点数运算

所以0.1 + 0.2 为什么出现精度失真呢?

首先,十进制的0.1和0.2会被转换成二进制的,但是由于浮点数用二进制表示时是无穷的:

0.1 -> 0.0001 1001 1001 1001...(1100循环) 0.2 -> 0.0011 0011 0011 0011...(0011循环) 复制代码

EEE 754 标准的 64 位双精度浮点数的小数部分最多支持53位二进制位,所以两者相加之后得到二进制为:

0.0100110011001100110011001100110011001100110011001100 复制代码

因浮点数小数位的限制而截断的二进制数字,再转换为十进制,就成了0.30000000000000004。所以在进行算术计算时会产生误差。

解决方案

老生常谈的解决方案有如下几种:

使用toFixed

    最简单的方法就是使用toFixed来处理小数:

    (0.1 + 0.2).toFixed(1) = '0.3' 复制代码

这种虽然简便,但是存在一些结果不精准的问题。

1.35.toFixed(1) // 1.4 正确 1.335.toFixed(2) // 1.33 错误 1.3335.toFixed(3) // 1.333 错误 1.33335.toFixed(4) // 1.3334 正确 1.333335.toFixed(5) // 1.33333 错误 1.3333335.toFixed(6) // 1.333333 错误 复制代码 化整数运算

该方法的主要思路就是把,小数转化为整数再进行计算。

/** * 小数点后面保留第 n 位 * * @param x 做近似处理的数 * @param n 小数点后第 n 位 * @returns 近似处理后的数 */ function roundFractional(x, n) { return Math.round(x * Math.pow(10, n)) / Math.pow(10, n); } 复制代码

思路:n则是要保留的小数,先扩大10^n,用Math.round把计算结果向上取证处理,然后除以10^n。

结果不仅没有变大,而且确保是整数兜底。如果想向下取证则使用Math.floor函数即可。

转字符串

大部分第三方库就是基于该方法进行封装,并且支持大数的处理。推荐使用。

/*** method ** * add / subtract / multiply /divide * floatObj.add(0.1, 0.2) >> 0.3 * floatObj.multiply(19.9, 100) >> 1990 * */ var floatObj = function() { /* * 判断obj是否为一个整数 */ function isInteger(obj) { return Math.floor(obj) === obj } /* * 将一个浮点数转成整数,返回整数和倍数。如 3.14 >> 314,倍数是 100 * @param floatNum {number} 小数 * @return {object} * {times:100, num: 314} */ function toInteger(floatNum) { var ret = {times: 1, num: 0} if (isInteger(floatNum)) { ret.num = floatNum return ret } var strfi = floatNum + '' var dotPos = strfi.indexOf('.') var len = strfi.substr(dotPos+1).length var times = Math.pow(10, len) var intNum = Number(floatNum.toString().replace('.','')) ret.times = times ret.num = intNum return ret } /* * 核心方法,实现加减乘除运算,确保不丢失精度 * 思路:把小数放大为整数(乘),进行算术运算,再缩小为小数(除) * * @param a {number} 运算数1 * @param b {number} 运算数2 * @param digits {number} 精度,保留的小数点数,比如 2, 即保留为两位小数 * @param op {string} 运算类型,有加减乘除(add/subtract/multiply/divide) * */ function operation(a, b, digits, op) { var o1 = toInteger(a) var o2 = toInteger(b) var n1 = o1.num var n2 = o2.num var t1 = o1.times var t2 = o2.times var max = t1 > t2 ? t1 : t2 var result = null switch (op) { case 'add': if (t1 === t2) { // 两个小数位数相同 result = n1 + n2 } else if (t1 > t2) { // o1 小数位 大于 o2 result = n1 + n2 * (t1 / t2) } else { // o1 小数位 小于 o2 result = n1 * (t2 / t1) + n2 } return result / max case 'subtract': if (t1 === t2) { result = n1 - n2 } else if (t1 > t2) { result = n1 - n2 * (t1 / t2) } else { result = n1 * (t2 / t1) - n2 } return result / max case 'multiply': result = (n1 * n2) / (t1 * t2) return result case 'divide': result = (n1 / n2) * (t2 / t1) return result } } // 加减乘除的四个接口 function add(a, b, digits) { return operation(a, b, digits, 'add') } function subtract(a, b, digits) { return operation(a, b, digits, 'subtract') } function multiply(a, b, digits) { return operation(a, b, digits, 'multiply') } function divide(a, b, digits) { return operation(a, b, digits, 'divide') } // exports return { add: add, subtract: subtract, multiply: multiply, divide: divide } }(); 复制代码 第三方库(bignumber.js)源码浅析 x = new Big(123.4567) y = Big('123456.7e-3') // 'new' is optional z = new Big(x) x.eq(y) && x.eq(z) && y.eq(z) ​ ​ 0.3 - 0.1 // 0.19999999999999998 x = new Big(0.3) x.minus(0.1) // "0.2" x // "0.3" function Big(n) { var x = this; ​ // 支持函数调用方式进行初始化,可以不使用new操作符 if (!(x instanceof Big)) return n === UNDEFINED ? _Big_() : new Big(n); ​ // 原型链判断,确认传入值是否已经为Big类的实例 if (n instanceof Big) { x.s = n.s; x.e = n.e; x.c = n.c.slice(); } else { if (typeof n !== 'string') { if (Big.strict === true) { throw TypeError(INVALID + 'number'); } ​ // 确定是否为-0,如果不是,转化为字符串. n = n === 0 && 1 / n < 0 ? '-0' : String(n); } ​ // parse函数只接受字符串参数 parse(x, n); } ​ x.constructor = Big; } function parse(x, n) { var e, i, nl; ​ if (!NUMERIC.test(n)) { throw Error(INVALID + 'number'); } ​ // 判断符号,是正数还是负数 x.s = n.charAt(0) == '-' ? (n = n.slice(1), -1) : 1; ​ // 判断是否有小数点 if ((e = n.indexOf('.')) > -1) n = n.replace('.', ''); ​ // 判断是否为科学计数法 if ((i = n.search(/e/i)) > 0) { ​ // 确定指数值 if (e < 0) e = i; e += +n.slice(i + 1); n = n.substring(0, i); } else if (e < 0) { ​ // 是一个正整数 e = n.length; } ​ nl = n.length; ​ // 确定数字前面有没有0,例如0123这种0 for (i = 0; i < nl && n.charAt(i) == '0';) ++i; ​ if (i == nl) { ​ // Zero. x.c = [x.e = 0]; } else { ​ // 确定数字后面的0,例如1.230这种0 for (; nl > 0 && n.charAt(--nl) == '0';); x.e = e - i - 1; x.c = []; ​ // 把字符串转换成数组进行存储,这个时候已经去掉了前面的0和后面的0 for (e = 0; i 0) { ye = xe; t = yc; } else { a = -a; t = xc; } ​ t.reverse(); for (; a--;) t.push(0); t.reverse(); } ​ // 把xc放到一个更长的数组中,方便后续循环加法操作 if (xc.length - yc.length < 0) { t = yc; yc = xc; xc = t; } ​ a = yc.length; ​ // 执行加法操作,将数值保存到xc中 for (b = 0; a; xc[a] %= 10) b = (xc[--a] = xc[a] + yc[a] + b) / 10 | 0; ​ // 不需要检查0,因为 +x + +y != 0 ,同时 -x + -y != 0 ​ if (b) { xc.unshift(b); ++ye; } ​ // 删除结尾的0 for (a = xc.length; xc[--a] === 0;) xc.pop(); ​ y.c = xc; y.e = ye; ​ return y; }; P.times = P.mul = function (y) { var c, x = this, Big = x.constructor, xc = x.c, yc = (y = new Big(y)).c, a = xc.length, b = yc.length, i = x.e, j = y.e; ​ // 符号比较确定最终的符号是为正还是为负 y.s = x.s == y.s ? 1 : -1; ​ // 如果有一个值是0,那么返回0即可 if (!xc[0] || !yc[0]) return new Big(y.s * 0); ​ // 小数点初始化为x.e+y.e,这是我们在两个小数相乘的时候,小数点的计算规则 y.e = i + j; ​ // 这一步也是保证xc的长度永远不小于yc的长度,因为要遍历xc来进行运算 if (a < b) { c = xc; xc = yc; yc = c; j = a; a = b; b = j; } ​ // 用0来初始化结果数组 for (c = new Array(j = a + b); j--;) c[j] = 0; ​ // i初始化为xc的长度 for (i = b; i--;) { b = 0; ​ // a是yc的长度 for (j = a + i; j > i;) { ​ // xc的一位乘以yc的一位,得到最终的结果值,保存下来 b = c[j] + yc[i] * xc[j - i - 1] + b; c[j--] = b % 10; ​ b = b / 10 | 0; } ​ c[j] = b; } ​ // 如果有进位,那么就调整小数点的位数(增加y.e),否则就删除最前面的0 if (b) ++y.e; else c.shift(); ​ // 删除后面的0 for (i = c.length; !c[--i];) c.pop(); y.c = c; ​ return y; }; P.round = function (dp, rm) { if (dp === UNDEFINED) dp = 0; else if (dp !== ~~dp || dp < -MAX_DP || dp > MAX_DP) { throw Error(INVALID_DP); } return round(new this.constructor(this), dp + this.e + 1, rm); }; ​ function round(x, sd, rm, more) { var xc = x.c; ​ if (rm === UNDEFINED) rm = Big.RM; if (rm !== 0 && rm !== 1 && rm !== 2 && rm !== 3) { throw Error(INVALID_RM); } ​ if (sd < 1) { // 兜底情况,精度小于1,默认有效值为1 more = rm === 3 && (more || !!xc[0]) || sd === 0 && ( rm === 1 && xc[0] >= 5 || rm === 2 && (xc[0] > 5 || xc[0] === 5 && (more || xc[1] !== UNDEFINED)) ); ​ xc.length = 1; ​ if (more) { ​ // 1, 0.1, 0.01, 0.001, 0.0001 等等 x.e = x.e - sd + 1; xc[0] = 1; } else { // 定义为0 xc[0] = x.e = 0; } } else if (sd < xc.length) { ​ // xc数组中,在精度之后的纸会被舍弃取整 more = rm === 1 && xc[sd] >= 5 || rm === 2 && (xc[sd] > 5 || xc[sd] === 5 && (more || xc[sd + 1] !== UNDEFINED || xc[sd - 1] & 1)) || rm === 3 && (more || !!xc[0]); ​ // 删除所需精度后的数组值 xc.length = sd--; ​ // 取整方式判断 if (more) { ​ // 四舍五入可能意味着前一个数字必须四舍五入,所以这个时候需要填0 for (; ++xc[sd] > 9;) { xc[sd] = 0; if (!sd--) { ++x.e; xc.unshift(1); } } } ​ // 删除小数点后面的0 for (sd = xc.length; !xc[--sd];) xc.pop(); } ​ return x; } 复制代码

在正常的逻辑中,我们根据精度舍弃了精度后的值,统一填充0进行表示。

通过内部的round函数的实现可以看到,在最开始我们进行了异常的兜底检测,排除了两种异常的情况。一种是参数错误,直接抛出异常;另一种是精度小于1的情况,在这个时候,定义了兜底的值1。

在big.js中,所有的取整运算都调用了内部的一个round函数。那么,接下来,我们就以API中的round方法为例。这个方法有两个参数,第一个值dp代表着小数后有效值的位数,第二个rm代表了取整的方式。

参考资料:

浮点数的二进制表示(IEEE 754标准) IEEE 754 - 维基百科,自由的百科全书 JS中浮点数精度问题 - 掘金 segmentfault.com/a/119000001…


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3