作者:吴志春
续上篇 JavaScript的数字存储 我们知道了为何0.1+0.2≠0.3,发现了问题,那么这次我们来解决问题。
分析问题
在解决问题之前,我们先来弄清楚问题的根源。出现的问题的根源,整数是因为数据太大;小数是因为小数在转换成二进制时出现了无效循环的情况,由于存储位数限制因此存在“舍去”,精度丢失就发生了,。详细请看上一篇文章 JavaScript的数字存储。
解决方案
对于整数,前端出现问题的几率可能比较低,毕竟很少有业务需要需要用到超大整数,只要运算结果不超过 Math.pow(2, 53) 就不会丢失精度。
对于小数,前端出现问题的几率还是很多的,尤其在一些电商网站涉及到金额等数据。解决方式:把小数放到位整数(乘倍数),再缩小回原来倍数(除倍数)。
例子
计算小数:0.1 + 0.02
计算方案:把小数放到位整数(乘倍数),再缩小回原来倍数(除倍数)。
计算过程
第一步把小数扩大成整数
0.1 扩大10倍 变成 1
0.02 扩大100倍 变成 2
第二步计算
1 * 100 ÷10 + 2 = 12
第三步缩小倍数(除以最大倍数)
12 ÷ 100 = 0.12
计算过程我们大概已经理清了,现在我们使用代码实现整个过程
代码实现
分析以上三个步骤,我们的代码实现分为两步。
第一步
为通过两个数字获取两个整数值,两个倍数值,以及最大倍数值。
例如:
入参为0.1 和0.02
则返回值应该是0.1的整数值1 ,倍数为10;0.02的整数值2,倍数值100。
代码实现如下
/**
* 判断num是否为一个整数
* @param {number|string|null} value
*/
isInteger = (value) => {
if (isNaN(value)) return false;
const num = +value;
return Math.floor(num) === num;
};
/**
* 将一个浮点数转成整数,返回整数和倍数
* @param {number|string|null} floatNum 如: 3.14
* @returns {object} 如:{times:100, num: 314}
*/
toInteger = (floatNum) => {
const ret = { times: 1, num: 0 };
if (this.isInteger(floatNum)) {
ret.num = floatNum;
return ret;
}
const strfi = `${floatNum}`;
const dotPos = strfi.indexOf('.');
const len = strfi.substr(dotPos + 1).length;
const times = 10 ** len;
ret.times = times;
ret.num = +strfi.replace('.', '');
return ret;
};
第二步,使用整数值以及倍数计算结果
代码如下
add = (prevValue, nextValue) => {
let result = null;
//初始化
const objectNumberA = this.toInteger(prevValue);
const objectNumberB = this.toInteger(nextValue);
const numberA = objectNumberA.num;
const numberB = objectNumberB.num;
const timesA = objectNumberA.times;
const timesB = objectNumberB.times;
const timesMax = timesA > timesB ? timesA : timesB;
//计算加法
if (timesA === timesB) {
// 两个小数位长度相同
result = numberA + numberB;
} else if (timesA > timesB) {
// numberA 小数位长度 大于 numberB
result = numberA + numberB * (timesA / timesB);
} else {
// numberA 小数位长度 小于 numberB
result = numberA * (timesB / timesA) + numberB;
}
//缩小并返回值
return result / timesMax;
};
代码测试
这样我们就小数加法丢失精度的问题。
同理我们可以实现减法,乘法,除法。具体实现代码入下,建议在有加法的基础上试试自己实现。
减法
subtract = (prevValue, nextValue) => {
let result = null;
//初始化
const objectNumberA = this.toInteger(prevValue);
const objectNumberB = this.toInteger(nextValue);
const numberA = objectNumberA.num;
const numberB = objectNumberB.num;
const timesA = objectNumberA.times;
const timesB = objectNumberB.times;
const timesMax = timesA > timesB ? timesA : timesB;
//计算减法
const { numberA, numberB, timesA, timesB, timesMax } = calcuNumber;
if (timesA === timesB) {
result = numberA - numberB;
} else if (timesA > timesB) {
result = numberA - numberB * (timesA / timesB);
} else {
result = numberA * (timesB / timesA) - numberB;
}
//缩小并输出
return result / timesMax;
};
乘法
multiply = (prevValue, nextValue) => {
//初始化
const objectNumberA = this.toInteger(prevValue);
const objectNumberB = this.toInteger(nextValue);
const numberA = objectNumberA.num;
const numberB = objectNumberB.num;
const timesA = objectNumberA.times;
const timesB = objectNumberB.times;
const timesMax = timesA > timesB ? timesA : timesB;
//计算乘法,缩小并输出
return (numberA * numberB) / (timesA * timesB);
};
除法
divide = (prevValue, nextValue) => {
//初始化
const objectNumberA = this.toInteger(prevValue);
const objectNumberB = this.toInteger(nextValue);
const numberA = objectNumberA.num;
const numberB = objectNumberB.num;
const timesA = objectNumberA.times;
const timesB = objectNumberB.times;
const timesMax = timesA > timesB ? timesA : timesB;
//计算除法,缩小并输出
return (numberA / numberB) * (timesB / timesA);
};
我们发现初始化代码会有重复,我们可以抽出来做成共用方法,这一步交给你们了。