JavaScript小數在做四則運算時,精度會丟失,這會在項目中引起諸多不便。先看個具體的例子:
//較小的數運算 console.log(0.09999999 + 0.00000001); //0.09999999999999999 console.log(-0.09999999 - 0.00000001); //-0.09999999999999999 console.log(0.012345 * 0.000001); //1.2344999999999999e-8 console.log(0.000001 / 0.0001); //0.009999999999999998 //較大的數運算 console.log(999999999 * 111111111); //111111110888888900
從上面的結果可以看出,都不是正確的。為了解決浮點數運算不准確的問題,在運算前我們把參加運算的數先升級(10的X的次方)到整數,等運算完后再降級(0.1的X
的次方)。具體的操作如下:
/** * 四則運算 * * @param x * @param y * @param op 操作符,0:加;1:減;2:乘;3:除 * @param acc 保留小數位個數,進行四舍五入 */ function execute(x, y, op, acc) { var xx = Number(x == undefined ? 0 : x); var yy = Number(y == undefined ? 0 : y); // var a = science(xx); var b = science(yy); var na = a.r1; var nb = b.r1; var ta = a.r2; var tb = b.r2; var maxt = Math.max(ta, tb); //精度值處理 var result = 0; switch (parseInt(op, 10)) { case 0: //加 result = (xx * maxt + yy * maxt) / maxt; break; case 1: //減 result = (xx * maxt - yy * maxt) / maxt; break; case 2: // 乘 result = na * nb / (ta * tb); break; case 3: // 除 result = na / nb * (tb / ta); default: } //小數位數處理 if (acc) { return Number(Number(result).toFixed(parseInt(acc))); } else { return Number(result); } }
/** * 將數值升級(10的X的次方)到整數 */ function science(num) { var re = { r1: 0, //數字去掉小數點后的值,也就是 r1*r2 的結果 r2: 1 //小數部分,10的長度次冪 }; if (isInteger(num)) { //整數直接返回 re.r1 = num; return re; } var snum = scienceNum(num + ""); //處理0.123e-10類似問題 var dotPos = snum.indexOf("."); //小數點位置 var len = snum.substr(dotPos + 1).length; //小數點長度 re.r2 = Math.pow(10, len); re.r1 = parseInt(snum.replace(".", "")); return re; }
/** * 將數值轉為字符串 * * 通過移動小數點 擴大倍數或縮小倍數(解決出現e+、e-的問題) * * JavaScript在以下情景會自動將數值轉換為科學計數法: * 1)小數點前的數字多於21位。 * 2)小數點后的零多於5個 */ function scienceNum(value) { if (!value) { return value; } if (typeof value === 'number') { value = value + "" }; let eIndex = value.indexOf('E'); if (eIndex == -1) { eIndex = value.indexOf('e') }; if (eIndex != -1) { let doubleStr = value.substring(0, eIndex); //e前面的值 let eStr = parseInt(value.substring(eIndex + 1, value.length)); //e后面的值 let doubleStrArr = doubleStr.split('.'); let doubleStr1 = doubleStrArr[0] || ""; let doubleStr2 = doubleStrArr[1] || ""; if (eStr < 0) { //e- 很小的數 let str1Len = doubleStr1.length; let eStrs = Math.abs(eStr); if (str1Len > eStrs) { let nums = doubleStr1.substring(0, eStrs); let nume = doubleStr1.substring(eStrs, str1Len); doubleStr = nums + "." + nume + nume; } else if (str1Len < eStrs) { let indexNum = eStrs - str1Len; let str = _makeZero(indexNum); //用0補齊 doubleStr = '0.' + str + doubleStr1 + doubleStr2; } else { doubleStr = '0.' + doubleStr1 + doubleStr2; } } else { //e+ 很大的數 let str2Len = doubleStr2.length; if (str2Len > eStr) { let _nums = doubleStr2.substring(0, eStr); let _nume = doubleStr2.substring(eStr, str2Len); doubleStr = doubleStr1 + _nums + '.' + _nume; } else if (str2Len < eStr) { let _indexNum = eStr - str2Len; let _str = _makeZero(_indexNum); //用0補齊 doubleStr = doubleStr1 + doubleStr2 + _str; } else { doubleStr = doubleStr1 + doubleStr2; } } value = doubleStr; } return value; }
//生成num個0的字符串 function _makeZero(num) { var str = ''; for (var i = 0; i < num; i++) { str += '0'; } return str; } /** * 判斷是否為整數,字符整數也返回true * * @param num * @returns */ function isInteger(num) { return Math.floor(num) === Number(num); }
為了調用方便,我們也可以增加如下幾個方法:
//加法運算 function add(x, y, acc) { return execute(x, y, 0, acc); } //減法運算 function subtract(x, y, acc) { return execute(x, y, 1, acc); } //乘法運算 function multiply(x, y, acc) { return execute(x, y, 2, acc); } //除法運算 function divide(x, y, acc) { return execute(x, y, 3, acc); }
在上面的計算中,toFixed 方法默認采用四舍六入五成雙算法。看個具體的例子:
console.log(Number(9.8350).toFixed(2)); //9.84 console.log(Number(9.8351).toFixed(2)); //9.84 console.log(Number(9.8250).toFixed(2)); //9.82 console.log(Number(9.82501).toFixed(2)); //9.83
如果想改成四舍五入,可以重寫該方法:
/** * 默認toFixed方法為四舍六入五成雙算法 * 重寫toFixed方法調整為四舍五入算法 */ Number.prototype.toFixed = function (d) { var s = this + ""; if (!d) d = 0; if (typeof d == 'string') { d = Number(d); }; if (s.indexOf(".") == -1) { s += "."; }; s = scienceNum(s); //處理e+、e-情況 s += new Array(d + 1).join("0"); if (new RegExp("^(-|\\+)?(\\d+(\\.\\d{0," + (d + 1) + "})?)\\d*$").test(s)) { var _s = "0" + RegExp.$2, pm = RegExp.$1, a = RegExp.$3.length, b = true; if (a == d + 2) { a = _s.match(/\d/g); if (parseInt(a[a.length - 1]) > 4) { for (var i = a.length - 2; i >= 0; i--) { a[i] = parseInt(a[i]) + 1; if (a[i] == 10) { a[i] = 0; b = i != 1; } else break; } } _s = a.join("").replace(new RegExp("(\\d+)(\\d{" + d + "})\\d$"), "$1.$2"); } if (b) { _s = _s.substr(1); }; return (pm + _s).replace(/\.$/, ""); } return this + ""; };
測試一下:
console.log(Number(9.8350).toFixed(2)); //9.84 console.log(Number(9.8351).toFixed(2)); //9.84 console.log(Number(9.8250).toFixed(2)); //9.83 console.log(Number(9.82501).toFixed(2)); //9.83