toFixed数字精度丢失问题怎么解决?银行家舍入规则是什么?

在编程时,数字的精度问题一直是令开发者们头痛的难题之一。特别是当涉及到浮点数运算时,即便是最基础的操作也可能因为精度损失而导致意想不到的结果。其中,toFixed 方法作为 JavaScript 中常用的数字格式化工具,其背后隐藏的精度问题常常让开发者感到困惑。本文将深入探讨 toFixed 方法导致的数字精度丢失问题,从存储、运算和显示三个方面逐一解析,并给出相应的解决方案。

toFiexed数字精度为什么会丢失

在使用toFiexed的过程中,你是否思考过这几个问题:为什么2.55.toFixed(1)的结果是2.5而2.45.toFixed(1)的结果也是2.5?toFixed不是“四舍六入五取偶(四舍六入五留双)’的舎入规则,也就是所谓的银行家舎入规则吗?

事实上,银行家舍入规则是四舍六入五考虑,五后非空就进一,五后为空看奇偶,五前为偶应舍去,五前为奇要进一。

这就会引发数字精度问题,数字的不精确体现在三个方面:存储、运算和显示

1.存储:

把一个十进制转换成二进制就会导致无限循环

0.2.toString(2) = 0.001100110011001100110011001100110011001100110011001101 由于计算机的存储能力有限,一般都是有个固定的精度,超过精度部分进行舍入。所以保存在计算机的时候保留20位(0.2.toPrecision(20))是0.20000000000000001110。

所以0.2000000000000000000001 === 0.1999999999999999999999999 (toString(2)相等)

思考:但是为什么0.25.toString(2)又是正确的0.01呢?

2.运算:

0.1 + 0.2 = 0.30000000000000004

0.2 + 0.3 = 0.5

思考:为什么0.2、0.3都是不精确的,计算结果却是精确的?

0.3 – 0.2 = 0.09999999999999998

0.2 – 0.3 = -0.09999999999999998

但如果是用精确存储的小数计算会是正确的:0.25 + 0.125 = 0.375

3.显示

思考:为什么0.2的存储是不精确的,但是var a = 0.2,console.log(a)又会是精确的呢?

因为计算机在显示的时候会做一个近似处理。那为什么0.1 + 0.2 的时候又没有近似处理呢?

为什么toFixed不精确?因为toFixed是先运算再显示的。

ECMAScript® 2015 Language Specification(Standard ECMA-2626th Edition,即 ES6)中关于 Number.prototype.toFixed 描述如下:

Number.prototype.toFixed(fractionDigits)

The following steps are performed:

    1. Let x be thisNumberValue(this value).
    2. ReturnIfAbrupt(x).
    3. Let f be ToInteger(fractionDigits). (If fractionDigits is undefined, this step produces the value 0).
    4. ReturnIfAbrupt(f).
    5. If f < 0 or f > 20, throw a RangeError exception. However, an implementation is permitted to extend the behaviour of toFixed for values of f less than 0 or greater than 20. In this case toFixed would not necessarily throw RangeError for such values.
    6. If x is NaN, return the String "NaN".
    7. Let s be the empty String.
    8. If x < 0, then
        Let s be "-".
        Let x = –x.
    9. If x ≥ 10^21, then
        Let m = ToString(x).
    10.Else x < 1021,
         a. Let n be an integer for which the exact mathematical value of n ÷ 10f – x is as close to zero as possible. If there are two such n, pick the larger n.
         b. If n = 0, let m be the String "0". Otherwise, let m be the String consisting of the digits of the decimal representation of n (in order, with no leading zeroes).
         c. If f ≠ 0, then
            i.  Let k be the number of elements in m.
            ii. If k ≤ f, then
                1. Let z be the String consisting of f+1–k occurrences of the code unit 0x0030.
                2. Let m be the concatenation of Strings z and m.
                3. Let k = f + 1.
            iii. Let a be the first k–f elements of m, and let b be the remaining f elements of m.
            iv.  Let m be the concatenation of the three Strings a, ".", and b.
    11. Return the concatenation of the Strings s and m.

ECMAScript® 2021 中关于此部分的描述与上面略有差异,但不影响结果。

我们将 1.335.toFixed(2) 值代入:

Number.prototype.toFixed ( fractionDigits ) => 1.335.toFixed(2)

1. x = thisNumberValue(1.335),x = 1.335
2. ReturnIfAbrupt(x),返回 x
3. f = ToInteger(fractionDigits),fractionDigits = 2,f = 2
4. ReturnIfAbrupt(f),返回 f
5. 如果 f < 0 或 f > 20,抛出 RangeError 异常。具体的实现允许扩展 toFxied 的行为,以支持 f < 0 或 f > 20 的情况,此时不会抛出异常。由于 f = 2 所以不会异常
6. 如果 x 是 NaN,则就会返回字符串型的 NaN(不满足)
7. s = ''
8. 如果 x < 0:(不满足)
   a. 则s变成"-"
   b. x = -x
9. 如果 x >= 10^21 则令 m = ToString(x)(不满足)
10. 如果 x < 10^21(x = 1.335,满足)
    a. 使 n 为整数以满足 n / 10^f – x 尽可能的接近于 0,如果存在两个这样的 n,选择较大的。
       候选的 n 有 132、133、134, n/10^2 - 1.335 计算结果如下: 
        ' 132/100-1.335 ' —— -0.014999999999999902
        ' 133/100-1.335 ' —— -0.004999999999999893
        ' 134/100-1.335 ' ——  0.0050000000000001155
        当 n 的值为 133 时最接近 0,所以 n = 133
    b. 如果 n = 0,则让 m = "0",否则,让 m 是由 n 的十进制表示的数字组成的字符串(按顺序,没有前导零),所以 m = 133
    c. 如果 f ≠ 0, 则(f = 2,满足)
        i. k 为 m 的数字个数,所以 k = 3,
       ii. 如果 k < f 则(k = 3,f = 2,不满足)
           1. 设z是由代码单元 0x0030的f +1– k次出现组成的字符串。
           2. 让m是字符串z和m的串联。
           3. 令k = f + 1。
       iii. a 为 m 的前 k-f(3-2)个元素,所以 a = 1,b 为 m 的余下元素,所以 b = 33,
        iv. m 为 a、'.' 和 b 的连接,所以 m = 1.33
11. 返回字符串 s('')和 m(1.33)的串联,即 1.33

所以 1.335.toFixed(2) = 1.33

解决精度计算的问题:用外部封装第三方包来解决,例如mathjs

结语

toFixed 方法在 JavaScript 中的使用虽然方便,但其背后隐藏的精度问题却不容忽视。从数字的存储、运算到最终的显示,每一个环节都可能导致精度的损失。了解这些原理不仅能帮助研发人员更好地理解 JavaScript 的行为,还能在开发中采取更加精准和可靠的策略来处理数字。对于需要高度精确计算的应用场景,推荐使用专门的数学库(如 mathjs)来替代 JavaScript 原生的数字处理方法,从而避免不必要的精度问题。

延展阅读:

JavaScript运行时有什么新选择?Bun 1.0和Node.js与Deno相比有什么优势?

JavaScript深拷贝与浅拷贝的区别与实现方法有哪些?

在javascript中,”??“和”?“有什么区别?

咨询方案 获取更多方案详情                        
(0)
研发专家-小鹿角研发专家-小鹿角
上一篇 2024年9月10日 下午5:36
下一篇 2024年9月11日 下午4:43

相关推荐