解决js计算浮点数精确值的问题

835 阅读3分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

作为一个前端开发人员,相信很多人在使用js计算价格等操作的时候,总会遇到浮点数的问题,经典场景如下:

image.png

为什么会出现上述浮点数问题呢?

原因在于对于浮点数的四则运算,几乎所有的编程语言都会有类似精度误差的问题,只不过在 C++/C#/Java 这些语言中已经封装好了方法来避免精度的问题,而 JavaScript 是一门弱类型的语言,从设计思想上就没有对浮点数有个严格的数据类型,所以精度误差的问题就显得格外突出。

对于上述 0.1 + 0.2 !== 0.3的情况,我们来分析下计算过程:

我们都知道计算机只能读懂二进制,而不是十进制,计算机会先把 0.1 和 0.2 转换成二进制:

0.1 => 0.0001 1001 1001 1001…(无限循环)
0.2 => 0.0011 0011 0011 0011…(无限循环)

上面我们发现0.1和0.2转化为二进制之后,变成了一个无限循环的数字,这在现实生活中,无限循环我们可以理解,但计算机是不允许无限循环的,对于无限循环的小数,计算机会进行舍入处理。进行双精度浮点数的小数部分最多支持 52 位,所以两者相加之后得到这么一串 0.0100110011001100110011001100110011001100110011001100 因浮点数小数位的限制而截断的二进制数字,这时候,我们再把它转换为十进制,就成了 0.30000000000000004。

应该如何解决这个问题呢?

JS提供了一个 toFixed 方法,toFixed可以指定保留小数点的位数,我们尝试一下:

image.png

看上去还不错,但是toFixed 有个不足:按照四舍五入保留位数,对于金额计算这种严谨的计算来说,有失偏颇

image.png

要想数据更加精准,更加严谨,我们不得不想出其他办法:

把需要计算的数字升级(乘以10的n次幂)成计算机能够精确识别的整数,等计算完毕再降级(除以10的n次幂),这是大部分编程语言处理精度差异的通用方法

常见操作方法如下:

1. 加法函数

function add(a, b) {
    var c, d, e;
    try {
        c = a.toString().split(".")[1].length;
    } catch (f) {
        c = 0;
    }
    try {
        d = b.toString().split(".")[1].length;
    } catch (f) {
        d = 0;
    }
    return e = Math.pow(10, Math.max(c, d)), (mul(a, e) + mul(b, e)) / e;
}

2. 减法函数,

用来得到精确的减法结果 {javascript的减法结果会有误差,在两个浮点数相减的时候会比较明显}

SubFn (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))
	n = (r1 >= r2) ? r1 : r2
	return ((arg1 * m - arg2 * m) / m).toFixed(n)
},

3. 乘法函数

MulFn(a, b) {
	var c = 0,
	d = a.toString(),
	e = b.toString();
        try {
		c += d.split('.')[1].length;
	} catch (f) {
		// 后续操作
	}
	try {
		c += e.split('.')[1].length;
	} catch (f) {
		// 后续操作
	}
	return Number(d.replace('.', '')) * Number(e.replace('.', '')) / Math.pow(10, c);
}

4. 除法函数

function div(a, b) {
    var c, d, e = 0,
        f = 0;
    try {
        e = a.toString().split(".")[1].length;
    } catch (g) {}
    try {
        f = b.toString().split(".")[1].length;
    } catch (g) {}
    return c = Number(a.toString().replace(".", "")), d = Number(b.toString().replace(".", "")), mul(c / d, Math.pow(10, f - e));
}