JS toFixed()方法精度丢失原因及解决方法
JS toFixed()方法精度丢失
toFixed()方法可把Number四舍五入为指定小数位数的数字。但这个方法并不完全遵从四舍五入的规则,如
2.485.toFixed(2) //=>2.48
网上流行的说法是遵循银行家舍入法,即:四舍六入五成双。四舍六入字面意思,5的话看后面不为零就进一,如果为零看5前面的数是奇数还是偶数,奇数进一偶数舍弃。但经过实际测试发现也不是遵从这个规则(chrome下),如
2.485.toFixed(2) //=>2.48
1.485.toFixed(2) //=>1.49
啊咧?这是为什么呢
造成不精确的真正原因--计算机存储
众所周知,计算机底层是0和1的二进制数据,不能被(1/2)^n
表示的小数都无法精确的转化为二进制。如
0.25.toString(2) //转化为二进制=>0.01
2.485.toString(2) //转化为二进制=>10.011111000010100011110101110000101000111101011100001
1.485.toString(2) //转化为二进制=>1.0111110000101000111101011100001010001111010111000011
1.485和2.485转化成的二进制其实是无限不循环小数,但计算机存储的长度是有限的,就只能以以固定长度截断,截断位置的后面可能是0,也可能是1。
如果后面是0会直接截断,如果是1会进1,这就造成实际保存下来的数字是有偏差的。我们用toPrecision()函数,用30位精度近似表示一下这两个小数:
1.485.toPrecision(30) //=>1.48500000000000009769962616701,比实际大
2.485.toPrecision(30) //=>2.48499999999999987565502124198,比实际小
不止toFixed,所有的运算由于存储偏差
都可能会带来误差。
在这个例子中,为什么两个数的小数部分都一致,但结果却不一样呢?
这是因为,1转为二进制还是1,占一位;2转为二进制是10,占两位。而截断二进制无限小数时是以固定的长度截断的,就会造成截断位置不同,1.485截断位置后面是1,会进一,而2.485截断位置后面是0,舍弃了。
解决方法
在main入口重写toFixed方法,分几种情况来处理,思路:
先把小数转换为字符串,分别截取小数点的前后部分分别判断处理,假设保留n位小数
1.n位之后的数字小于5,舍:小数部分截取字符串n个字符返回
2.n位之后的数字大于等于5,进:截取后放大10^n
倍之后转为整数,正数加一,负数减一,再缩小10^n
倍
Number.prototype.toFixed = function (n) {
if (n > 20 || n < 0) { //精度允许0-20
throw new RangeError('toFixed() digits argument must be between 0 and 20');
}
const number = this;
if (isNaN(number) || number >= Math.pow(10, 21)) {
return number.toString();
}
if (typeof (n) == 'undefined' || n == 0) {
return (Math.round(number)).toString();
}
//判断是否为负数
var isMinus = number > 0 ? false : true;
let result = number.toString();
const arr = result.split('.');
// 整数的情况
if (arr.length < 2) {
result += '.';
for (let i = 0; i < n; i += 1) {
result += '0';
}
return result;
}
const integer = arr[0];
const decimal = arr[1];
//小数位数和精确位数相等时
if (decimal.length == n) {
return result;
}
//小数位数小于精确位数时
if (decimal.length < n) {
for (let i = 0; i < n - decimal.length; i += 1) {
result += '0';
}
return result;
}
result = integer + '.' + decimal.substr(0, n);
const last = decimal.substr(n, 1);
// 如果需要进,放大转换为整数再处理,避免浮点数精度的损失
if (parseInt(last, 10) >= 5) {
const x = Math.pow(10, n); //放大系数
// 对于过精度位的下一位值大于5时,正数+1 负数-1.
// 正数例如1.057 转化为两位精度的小数是 1.06。
// 负数例如-1.057 转化为两位精度的小数是 -1.06。
result = (Math.round((parseFloat(result) * x)) + (isMinus ? -1 : 1)) / x;
//为避免除以系数后小数部分0缺失,再调用一次方法。
//如1.299,保留两位小数,(1.29*100 + 1)/100=1.3,则需要再补一次零->1.30
result = result.toFixed(n);
}
return result;
};
本文作者:波特卡斯D
本文链接:https://www.cnblogs.com/zyj-Blogs/p/17838206.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步