一、场景复现
0.1 + 0.2 === 0.3 //false
为什么是false呢?先看下面这个比喻
比如一个数 1÷3=0.33333333......
3会一直无限循环,数学可以表示,但是计算机要存储,方便下次取出来再使用,但0.333333...... 这个数无限循环,再大的内存它也存不下,所以不能存储一个相对于数学来说的值,只能存储一个近似值,当计算机存储后再取出时就会出现精度丢失问题
二、问题分析
再回到问题上
0.1 + 0.2 === 0.3 //false
我们知道,在javascript语言中,0.1 和 0.2 都转化成二进制后再进行运算
// 0.1 和 0.2 都转化成二进制后再进行运算
0.00011001100110011001100110011001100110011001100110011010 +
0.0011001100110011001100110011001100110011001100110011010 =
0.0100110011001100110011001100110011001100110011001100111
// 转成十进制正好是 0.30000000000000004
三、解决方案
理论上用有限的空间来存储无限的小数是不可能保证精确的,但我们可以处理一下得到我们期望的结果
当你拿到 1.4000000000000001 这样的数据要展示时,建议使用 toPrecision 凑整并 parseFloat 转成数字后再显示,如下:
parseFloat(1.4000000000000001.toPrecision(12)) === 1.4 // True
封装成方法就是:
function strip(num, precision = 12) {
return +parseFloat(num.toPrecision(precision));
}
对于运算类操作,如 +-*/,就不能使用 toPrecision 了。正确的做法是把小数转成整数后再运算。以加法为例:
/**
* 精确加法
*/
function add(num1, num2) {
const num1Digits = (num1.toString().split('.')[1] || '').length;
const num2Digits = (num2.toString().split('.')[1] || '').length;
const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits));
return (num1 * baseNum + num2 * baseNum) / baseNum;
}
使用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 正确
化整数运算
该方法的主要思路就是把,小数转化为整数再进行计算。
/**
* 小数点后面保留第 n 位
*
* @param x 做近似处理的数
* @param n 小数点后第 n 位
* @returns 近似处理后的数
*/
function roundFractional(x, n) {
return Math.round(x * Math.pow(10, n)) / Math.pow(10, n);
}
思路:n则是要保留的小数,先扩大10n,用Math.round把计算结果向上取证处理,然后除以10n。
结果不仅没有变大,而且确保是整数兜底。如果想向下取证则使用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
安装
npm install bignumber.js --save
引入
npm install bignumber.js --save
使用number-precision解决
安装
npm install number-precision --save
引入
import NP from 'number-precision'
使用
NP.strip(0.09999999999999998); 转为最接近的正确的数字 // = 0.1
NP.plus(0.1, 0.2); 加法 至少需要2个参数 // = 0.3, 而不是原来错误的:0.30000000000000004
NP.minus(1.0, 0.9); 减法 // = 0.1, 而不是原来错误的:0.09999999999999998
NP.times(3, 0.3); 乘法 // = 0.9, 而不是原来错误的:0.8999999999999999
NP.divide(1.21, 1.1); 除法 // = 1.1, 而不是原来错误的:1.0999999999999999
NP.round(num, ratio) // 根据ratio取整
NP.round(0.105, 2); // = 0.11, 而不是原来错误的:0.1