首頁 >web前端 >js教程 >number()。tofixed()舍入錯誤:破裂但可固定

number()。tofixed()舍入錯誤:破裂但可固定

Jennifer Aniston
Jennifer Aniston原創
2025-02-15 10:02:12180瀏覽

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()中此版本的捨入錯誤。

<code class="language-javascript">(1.015).toFixed(2) // 返回 "1.01" 而不是 "1.02"</code>

失敗的測試位於此處第42行。直到2017年12月我才發現它,這促使我去檢查其他問題。

請參閱我關於此問題的推文:

  • 錯誤警報
  • 與Intl.NumberFormat進行比較
  • 總結
  • Polyfill

錯誤報告

關於使用toFixed()的捨入錯誤,有很長的錯誤報告歷史。

  • Chrome
  • Firefox
  • Firefox,另見
  • Internet Explorer

以下是一些關於此問題的StackOverflow問題的簡短示例:

  • 示例一
  • 示例二

通常,這些都指出了一個值的一個錯誤,但沒有報告返回錯誤結果的值的範圍或模式(至少我沒有找到,我可能錯過了什麼)。這使得程序員只能關注小問題而看不到更大的模式。我不責怪他們這樣。

查找模式

基於輸入的意外結果必須源於輸入中的共享模式。因此,我沒有審查Number().toFixed()的規範,而是專注於使用一系列值進行測試,以確定錯誤在每個系列中的出現位置。

測試函數

我創建了以下測試函數,以對一系列從1到maxValue的整數進行toFixed()運算,為每個整數添加例如.005的小數。 toFixed()的固定(數字位數)參數根據小數值的長度計算。

<code class="language-javascript">(1.015).toFixed(2) // 返回 "1.01" 而不是 "1.02"</code>

用法

以下示例對整數1到128執行操作,為每個整數添加小數.015,並返回一個“意外”結果數組。每個結果包含given、expected和actual字段。在這裡,我們使用該數組並打印每個項目。

<code class="language-javascript">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)
      }
    })
}</code>

輸出

對於這種情況,有6個意外結果。

<code class="language-javascript">test({ fraction: .015, maxValue: 128 })
  .forEach(function(item) {
    console.log(item)
  })</code>

發現

我發現此錯誤包含三個部分:

  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>

然後再次運行測試並檢查結果長度是否為零。

<code class="language-javascript">(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));</code>

或者只運行啟動這篇文章的初始轉換。

<code class="language-javascript">test({ fraction: .0015, maxValue: 516 }) // Array []
test({ fraction: .0015, maxValue: 516 }).length // 0</code>

感謝您的閱讀!

關於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