如果叫你四舍五入保留n位小数,你会用哪种方法呢,下面笔者会介绍几种方法供你参考。
原生 toFixed
嗯,如果用原生方法就能解决问题,那么写这篇文章有啥用 -。-。 toFixed 的缺点很明显,就是大多数浏览器无法准确计算以5为尾部的小数
let number = 123.345;
number.toFixed(2); // 123.34 chrome, firefox, safari 都是显示这个
Math.round
Math.round 可以对数字进行四舍五入成整数,可以利用这点进行一些简单的乘除运算达到保留n位小数点的效果。
function toFixed(num,decimal){
if(isNaN(num)){
return 0;
}
num = num-0;
var p1 = Math.pow(10, decimal + 1);
var p2 = Math.pow(10, decimal);
console.log(num * p1 / 10);
console.log(Math.round(num * p1 / 10));
return (Math.round(num * p1 / 10) / p2).toFixed(decimal);
}
上面代码的逻辑很简单,就是把一个数字num乘于10的(n+1)倍数再除以10转成只有一位小数的值,再用math.round进行四舍五入,最后除以10的(n)倍数得到对应的n位小数点的值。
上面的方法简单也实用,可惜有bug...看看下面的例子:
toFixed(4100.065,2); // 4100.06
// 4100065/10 = 410006.49999999994
原因是就是js编程语言的小数点精度问题,在上面除以10的过程中,某些数值产生了精度问题。
字符串处理
转成字符串处理可以避开精度问题,但是要自己模拟四舍五入。下面提供一个思路:
- 把数字转成字符,并记录
.小数点的位置;如果没有小数点,则直接在后面补0 - 通过小数点的位置,计算出原数字有多少位小数(oldPointNum);把小数点去掉并且把字符串转成数组
- 通过比较oldPointNum和n(要保留的位数);如果oldPointNum<n,直接补0;否则下一步
- 比较数组倒数(i =oldPointNum-n)的数字是否>=5;是则进一位;i++;循环该步骤(核心),用这一步来模拟四舍五入
const toFixed = (number, n) => {
let numberStr = number + "";
let reg = /^(-|\+)?(\d+(\.\d*)?|\.\d+)$/i;
if(!reg.test(numberStr)) {
console.error('输入的数字格式不对');
return;
}
let sign = numberStr.charAt(0) === '-' ? (numberStr=numberStr.slice(1),-1):1; // 判断是否是负数
let pointIndex = numberStr.indexOf("."); // 记录小数点的位置
if (pointIndex > -1) {
numberStr = numberStr.replace(".", "");
} else { // 没有小数点直接添加补0;
numberStr += ".";
numberStr+=new Array(n).join('0');
return numberStr;
}
let numberArray = numberStr.split(""); //转成数组
let len = numberArray.length;
let oldPointNum = len - pointIndex; // 获取原数据有多少位小数
if (oldPointNum < n) { // 要保留的小数点比原来的要大,直接补0
while (n - oldPointNum > 0) {
numberArray.push(0);
n--;
}
} else if (oldPointNum > n) { // 模拟四舍五入
let i = oldPointNum - n; // 从倒数第i个数字开始比较
let more = numberArray[len - i] >= 5 ? true : false;
while (more) {
i++;
more = +numberArray[len - i] + 1 === 10 ? true : false; // 进位后判断是否等于10,是则继续进位
numberArray[len - i] = more&&i!==(len+1) ? 0 : +numberArray[len - i] + 1; // 其他位置的数字进位直接变成0,第一位的例外
console.log(i, len);
}
numberArray.length = len- (oldPointNum-n); // 截取多余的小数
}
numberArray.splice(pointIndex, 0, ".");
return sign===-1?'-'+numberArray.join(""):numberArray.join("");
};
另外提供另一个版本,思路差不多,但是更多使用了正则:
const toFixed = (number,n)=>{
let numberStr = number + "";
if(!n) n=0;
if(numberStr.indexOf('.') === -1) numberStr+='.';
numberStr += new Array(n + 1).join("0");
let reg = new RegExp("^(-|\\+)?(\\d+(\\.\\d{0," + (n + 1) + "})?)\\d*$");
const matches = numberStr.match(reg);
if(!matches){
console.error('输入的数字格式不对');
return;
}
console.log(matches);
let sign = matches[1] || '';
let pointNum = matches[3];
numberStr = '0'+matches[2];
let isTop = true;
let len = pointNum.length;
if(len === n+2){
pointNum = numberStr.match(/\d/g);
len = pointNum.length;
if((+pointNum[len-1]) >=5){
for(let i=len-2;i>=0;i--){
pointNum[i] = (+pointNum[i]) +1;
if(pointNum[i] === 10){
pointNum[i] = 0;
isTop = i!==1;
}else break;
}
}
numberStr = pointNum.join('').replace(new RegExp("(\\d+)(\\d{" + n + "})\\d$"),'$1.$2');
}
if(isTop) numberStr = numberStr.slice(1);
return (sign+numberStr)
}
总结
总的来说,原生toFixed不靠谱;Math.round模拟的代码量少且简单,但是会有精度问题;用字符串模拟的代码量大(代码可能存在bug...),但是不会有精度问题。就看你想用哪种喽