JavaScript小数在做四则运算时,精度会丢失,这会在项目中引起诸多不便。先看个具体的例子:
1//较小的数运算 2 console.log(0.09999999 + 0.00000001); //0.09999999999999999 3 console.log(-0.09999999 - 0.00000001); //-0.09999999999999999 4 console.log(0.012345 * 0.000001); //1.2344999999999999e-8 5 console.log(0.000001 / 0.0001); //0.009999999999999998 6 7 //较大的数运算 8 console.log(999999999 * 111111111); //111111110888888900
从上面的结果可以看出,都不是正确的。为了解决浮点数运算不准确的问题,在运算前我们把参加运算的数先升级(10的X的次方)到整数,等运算完后再降级(0.1的X
的次方)。具体的操作如下:
1/** 2 * 四则运算 3 * 4 * @param x 5 * @param y 6 * @param op 操作符,0:加;1:减;2:乘;3:除 7 * @param acc 保留小数位个数,进行四舍五入 8 */ 9function execute(x, y, op, acc) { 10 var xx = Number(x == undefined ? 0 : x); 11 var yy = Number(y == undefined ? 0 : y); 12 13 // 14 var a = science(xx); 15 var b = science(yy); 16 17 var na = a.r1; 18 var nb = b.r1; 19 20 var ta = a.r2; 21 var tb = b.r2; 22 var maxt = Math.max(ta, tb); 23 24 //精度值处理 25 var result = 0; 26 switch (parseInt(op, 10)) { 27 case 0: //加 28 result = (xx * maxt + yy * maxt) / maxt; 29 break; 30 case 1: //减 31 result = (xx * maxt - yy * maxt) / maxt; 32 break; 33 case 2: // 乘 34 result = na * nb / (ta * tb); 35 break; 36 case 3: // 除 37 result = na / nb * (tb / ta); 38 default: 39 } 40 41 //小数位数处理 42 if (acc) { 43 return Number(Number(result).toFixed(parseInt(acc))); 44 } else { 45 return Number(result); 46 } 47} 48 49/** 50 * 将数值升级(10的X的次方)到整数 51 */ 52function science(num) { 53 var re = { 54 r1: 0, //数字去掉小数点后的值,也就是 r1*r2 的结果 55 r2: 1 //小数部分,10的长度次幂 56 }; 57 if (isInteger(num)) { //整数直接返回 58 re.r1 = num; 59 return re; 60 } 61 var snum = scienceNum(num + ""); //处理0.123e-10类似问题 62 var dotPos = snum.indexOf("."); //小数点位置 63 var len = snum.substr(dotPos + 1).length; //小数点长度 64 re.r2 = Math.pow(10, len); 65 re.r1 = parseInt(snum.replace(".", "")); 66 return re; 67} 68 69/** 70 * 将数值转为字符串 71 * 72 * 通过移动小数点 扩大倍数或缩小倍数(解决出现e+、e-的问题) 73 * 74 * JavaScript在以下情景会自动将数值转换为科学计数法: 75 * 1)小数点前的数字多于21位。 76 * 2)小数点后的零多于5个 77 */ 78function scienceNum(value) { 79 if (!value) { 80 return value; 81 } 82 if (typeof value === 'number') { 83 value = value + "" 84 }; 85 let eIndex = value.indexOf('E'); 86 if (eIndex == -1) { 87 eIndex = value.indexOf('e') 88 }; 89 if (eIndex != -1) { 90 let doubleStr = value.substring(0, eIndex); //e前面的值 91 let eStr = parseInt(value.substring(eIndex + 1, value.length)); //e后面的值 92 let doubleStrArr = doubleStr.split('.'); 93 let doubleStr1 = doubleStrArr[0] || ""; 94 let doubleStr2 = doubleStrArr[1] || ""; 95 96 if (eStr < 0) { //e- 很小的数 97 let str1Len = doubleStr1.length; 98 let eStrs = Math.abs(eStr); 99 if (str1Len > eStrs) { 100 let nums = doubleStr1.substring(0, eStrs); 101 let nume = doubleStr1.substring(eStrs, str1Len); 102 doubleStr = nums + "." + nume + nume; 103 } else if (str1Len < eStrs) { 104 let indexNum = eStrs - str1Len; 105 let str = _makeZero(indexNum); //用0补齐 106 doubleStr = '0.' + str + doubleStr1 + doubleStr2; 107 } else { 108 doubleStr = '0.' + doubleStr1 + doubleStr2; 109 } 110 } else { //e+ 很大的数 111 let str2Len = doubleStr2.length; 112 if (str2Len > eStr) { 113 let _nums = doubleStr2.substring(0, eStr); 114 let _nume = doubleStr2.substring(eStr, str2Len); 115 doubleStr = doubleStr1 + _nums + '.' + _nume; 116 } else if (str2Len < eStr) { 117 let _indexNum = eStr - str2Len; 118 let _str = _makeZero(_indexNum); //用0补齐 119 doubleStr = doubleStr1 + doubleStr2 + _str; 120 } else { 121 doubleStr = doubleStr1 + doubleStr2; 122 } 123 } 124 value = doubleStr; 125 } 126 return value; 127} 128 129//生成num个0的字符串 130function _makeZero(num) { 131 var str = ''; 132 for (var i = 0; i < num; i++) { 133 str += '0'; 134 } 135 return str; 136} 137 138/** 139 * 判断是否为整数,字符整数也返回true 140 * 141 * @param num 142 * @returns 143 */ 144function isInteger(num) { 145 return Math.floor(num) === Number(num); 146}
为了调用方便,我们也可以增加如下几个方法:
1//加法运算 2function add(x, y, acc) { 3 return execute(x, y, 0, acc); 4} 5 6//减法运算 7function subtract(x, y, acc) { 8 return execute(x, y, 1, acc); 9} 10 11//乘法运算 12function multiply(x, y, acc) { 13 return execute(x, y, 2, acc); 14} 15 16//除法运算 17function divide(x, y, acc) { 18 return execute(x, y, 3, acc); 19}
在上面的计算中,toFixed 方法默认采用四舍六入五成双算法。看个具体的例子:
1console.log(Number(9.8350).toFixed(2)); //9.84 2 console.log(Number(9.8351).toFixed(2)); //9.84 3 console.log(Number(9.8250).toFixed(2)); //9.82 4 console.log(Number(9.82501).toFixed(2)); //9.83
如果想改成四舍五入,可以重写该方法:
1/** 2 * 默认toFixed方法为四舍六入五成双算法 3 * 重写toFixed方法调整为四舍五入算法 4 */ 5Number.prototype.toFixed = function (d) { 6 var s = this + ""; 7 if (!d) d = 0; 8 if (typeof d == 'string') { 9 d = Number(d); 10 }; 11 if (s.indexOf(".") == -1) { 12 s += "."; 13 }; 14 s = scienceNum(s); //处理e+、e-情况 15 s += new Array(d + 1).join("0"); 16 if (new RegExp("^(-|\\+)?(\\d+(\\.\\d{0," + (d + 1) + "})?)\\d*$").test(s)) { 17 var _s = "0" + RegExp.$2, 18 pm = RegExp.$1, 19 a = RegExp.$3.length, 20 b = true; 21 if (a == d + 2) { 22 a = _s.match(/\d/g); 23 if (parseInt(a[a.length - 1]) > 4) { 24 for (var i = a.length - 2; i >= 0; i--) { 25 a[i] = parseInt(a[i]) + 1; 26 if (a[i] == 10) { 27 a[i] = 0; 28 b = i != 1; 29 } else break; 30 } 31 } 32 _s = a.join("").replace(new RegExp("(\\d+)(\\d{" + d + "})\\d$"), "$1.$2"); 33 } 34 if (b) { 35 _s = _s.substr(1); 36 }; 37 return (pm + _s).replace(/\.$/, ""); 38 } 39 return this + ""; 40};
测试一下:
1console.log(Number(9.8350).toFixed(2)); //9.84 2 console.log(Number(9.8351).toFixed(2)); //9.84 3 console.log(Number(9.8250).toFixed(2)); //9.83 4 console.log(Number(9.82501).toFixed(2)); //9.83