首页 > web前端 > js教程 > number()。tofixed()舍入错误:破裂但可固定

number()。tofixed()舍入错误:破裂但可固定

Jennifer Aniston
发布: 2025-02-15 10:02:12
原创
188 人浏览过

Number().toFixed() Rounding Errors: Broken But Fixable

本文最初发表于David Kaye博客

我在所有尝试过的JavaScript环境(Chrome、Firefox、Internet Explorer、Brave和Node.js)中都发现了Number().toFixed()中的舍入错误。令人惊讶的是,修复方法非常简单。继续阅读……

关键要点

  • JavaScript的Number().toFixed()函数在多个环境(Chrome、Firefox、Internet Explorer、Brave和Node.js)中存在舍入错误。此错误不一致,当小数部分的最后一位有效数字为5,且修整长度仅缩短小数部分一位,并应用不同的整数值时出现。
  • 当值包含小数时,可以通过在要修改的值的字符串版本末尾附加“1”来修复此错误。此解决方案无需重新审视较低级别的二进制或浮点运算。
  • 可以使用polyfill覆盖toFixed()函数,直到所有实现都被修改。但是,并非每个人都对此方法感到满意。polyfill涉及prototype.toFixed函数,并检查结果长度是否为零。

热身

在修改一个执行与Intl.NumberFormat#format()相同功能的数字格式化函数时,我发现了toFixed()中此版本的舍入错误。

(1.015).toFixed(2) // 返回 "1.01" 而不是 "1.02"
登录后复制
登录后复制

失败的测试位于此处第42行。直到2017年12月我才发现它,这促使我去检查其他问题。

请参阅我关于此问题的推文:

  • 错误警报
  • 与Intl.NumberFormat进行比较
  • 总结
  • Polyfill

错误报告

关于使用toFixed()的舍入错误,有很长的错误报告历史。

  • Chrome
  • Firefox
  • Firefox,另见
  • Internet Explorer

以下是一些关于此问题的StackOverflow问题的简短示例:

  • 示例一
  • 示例二

通常,这些都指出了一个值的一个错误,但没有报告返回错误结果的值的范围或模式(至少我没有找到,我可能错过了什么)。这使得程序员只能关注小问题而看不到更大的模式。我不责怪他们这样。

查找模式

基于输入的意外结果必须源于输入中的共享模式。因此,我没有审查Number().toFixed()的规范,而是专注于使用一系列值进行测试,以确定错误在每个系列中的出现位置。

测试函数

我创建了以下测试函数,以对一系列从1到maxValue的整数进行toFixed()运算,为每个整数添加例如.005的小数。toFixed()的固定(数字位数)参数根据小数值的长度计算。

(1.015).toFixed(2) // 返回 "1.01" 而不是 "1.02"
登录后复制
登录后复制

用法

以下示例对整数1到128执行操作,为每个整数添加小数.015,并返回一个“意外”结果数组。每个结果包含given、expected和actual字段。在这里,我们使用该数组并打印每个项目。

function test({fraction, maxValue}) {
  fraction = fraction.toString()
  var fixLength = fraction.split('.')[1].length - 1

  var last = Number(fraction.charAt(fraction.length - 1))
  var fixDigit = Number(fraction.charAt(fraction.length - 2))

  last >= 5 && (fixDigit = fixDigit + 1)

  var expectedFraction = fraction.replace(/[\d]{2,2}$/, fixDigit)

  return Array(maxValue).fill(0)
    .map(function(ignoreValue, index) {
      return index + 1
    })
    .filter(function(integer) {
      var number = integer + Number(fraction)
      var actual = number.toFixed(fixLength)
      var expected = Number(number + '1').toFixed(fixLength)

      return expected != actual
    })
    .map(function(integer) {
      var number = Number(integer) + Number(fraction)
      return {
        given: number.toString(),
        expected: (Number(integer.toFixed(0)) + Number(expectedFraction)).toString(),
        actual: number.toFixed(fixLength)
      }
    })
}
登录后复制

输出

对于这种情况,有6个意外结果。

test({ fraction: .015, maxValue: 128 })
  .forEach(function(item) {
    console.log(item)
  })
登录后复制

发现

我发现此错误包含三个部分:

  1. 小数部分的最后一位有效数字必须为5(.015和.01500产生相同的结果)。
  2. 修整长度必须仅缩短小数部分一位。
  3. 随着应用不同的整数值,错误不一致地出现。

不一致?

例如,对于整数1到128,(value).toFixed(2)使用以5结尾的不同3位小数,产生以下结果:

  • 以.005结尾的修整数字总是失败(!!)
  • 以.015结尾的修整数字对于1、然后4到7、然后128失败
  • 以.025结尾的修整数字对于1、2、3、然后16到63失败
  • 以.035结尾的修整数字对于1、然后32到128失败
  • 以.045结尾的修整数字对于1到15、然后128失败
  • 以.055结尾的修整数字对于1、然后4到63失败
  • 以.065结尾的修整数字对于1、2、3、然后8到15、然后32到128失败
  • 以.075结尾的修整数字对于1、然后8到31、然后128失败
  • 以.085结尾的修整数字对于1到7、然后64到127失败(!!)
  • 以.095结尾的修整数字对于1、然后4到7、然后16到128失败

那些比我更了解二进制和浮点数学知识的人可能能够推断出根本原因。我把这留给读者作为练习。

修复toFixed()

通过多于一位小数来修复值总是正确舍入;例如,(1.0151).toFixed(2)按预期返回“1.02”。测试和polyfill都使用该知识进行正确性检查。

这意味着所有toFixed()实现都有一个简单的修复方法:如果值包含小数,则在要修改的值的字符串版本末尾附加“1”。这可能不是“符合规范的”,但这意味着我们将获得预期的结果,而无需重新审视较低级别的二进制或浮点运算。

Polyfill

在所有实现都被修改之前,如果您愿意这样做(并非每个人都愿意),您可以使用以下polyfill覆盖toFixed()。

<code>Object { given: "1.015", expected: "1.02", actual: "1.01" }
Object { given: "4.015", expected: "4.02", actual: "4.01" }
Object { given: "5.015", expected: "5.02", actual: "5.01" }
Object { given: "6.015", expected: "6.02", actual: "6.01" }
Object { given: "7.015", expected: "7.02", actual: "7.01" }
Object { given: "128.015", expected: "128.02", actual: "128.01" }</code>
登录后复制

然后再次运行测试并检查结果长度是否为零。

(1.005).toFixed(2) == "1.01" || (function(prototype) {
  var toFixed = prototype.toFixed

  prototype.toFixed = function(fractionDigits) {
    var split = this.toString().split('.')
    var number = +(!split[1] ? split[0] : split.join('.') + '1')

    return toFixed.call(number, fractionDigits)
  }
}(Number.prototype));
登录后复制

或者只运行启动这篇文章的初始转换。

test({ fraction: .0015, maxValue: 516 }) // Array []
test({ fraction: .0015, maxValue: 516 }).length // 0
登录后复制

感谢您的阅读!

关于JavaScript的Number.toFixed()方法的常见问题解答 (FAQ)

JavaScript中的Number.toFixed()方法的目的是什么?

JavaScript中的Number.toFixed()方法用于使用定点表示法格式化数字。它返回数字的字符串表示形式,该表示形式不使用指数表示法,并且小数点后恰好有指定数量的位数。如有必要,将对数字进行舍入,并且结果字符串的小数点后长度特定。

为什么Number.toFixed()方法有时会给出不准确的结果?

由于JavaScript处理二进制浮点数的方式,Number.toFixed()方法有时会给出不准确的结果。JavaScript使用二进制浮点数,它无法准确表示所有十进制小数。当使用Number.toFixed()方法将数字舍入到特定的小数位数时,这种不准确性会导致意外结果。

如何修复Number.toFixed()方法中的舍入错误?

修复Number.toFixed()方法中的舍入错误的一种方法是使用自定义舍入函数。此函数可以考虑JavaScript的数字处理的特性并提供更准确的结果。例如,您可以使用一个函数,该函数将数字乘以10的幂,将其舍入到最接近的整数,然后将其除以相同的10的幂。

我可以将Number.toFixed()方法与非数值一起使用吗?

不可以,Number.toFixed()方法只能与数值一起使用。如果您尝试将其与非数值一起使用,JavaScript将抛出TypeError。如果您需要将此方法与可能不是数字的值一起使用,则应首先检查该值是否为数字。

Number.toFixed()方法与其他舍入方法之间是否存在性能差异?

Number.toFixed()方法与其他舍入方法之间的性能差异通常可以忽略不计。但是,如果您正在执行大量操作,则使用自定义舍入函数可能会比使用Number.toFixed()方法略快。

我可以使用Number.toFixed()方法舍入到超过20位小数吗?

不可以,Number.toFixed()方法只能舍入到最多20位小数。如果您尝试舍入到超过20位小数,JavaScript将抛出RangeError。

Number.toFixed()方法如何处理负数?

Number.toFixed()方法处理负数的方式与处理正数的方式相同。它将数字的绝对值舍入到指定的小数位数,然后添加负号。

我可以在所有JavaScript环境中使用Number.toFixed()方法吗?

是的,Number.toFixed()方法是JavaScript的标准部分,应该在所有JavaScript环境中可用。但是,由于不同JavaScript引擎处理数字的方式不同,因此结果在所有环境中可能并不完全相同。

如果我不向Number.toFixed()方法传递任何参数会发生什么?

如果您不向Number.toFixed()方法传递任何参数,它将默认舍入到0位小数。这意味着它将把数字舍入到最接近的整数。

我可以将Number.toFixed()方法与大数一起使用吗?

是的,您可以将Number.toFixed()方法与大数一起使用。但是,请记住,JavaScript只能准确表示最大为2^53 – 1的数字。如果您尝试将Number.toFixed()方法与大于此数字的数字一起使用,则它可能不会给出准确的结果。

以上是number()。tofixed()舍入错误:破裂但可固定的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
作者最新文章
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板