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.toString(2) //=>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;
};
posted @ 2023-11-17 11:02  波特卡斯D  阅读(654)  评论(0编辑  收藏  举报