JS中小数点精度丢失的原因及解决方法


1、为什么会出现计算精度丢失的问题?

JavaScript中存在小数点精度丢失的问题是由于其使用的浮点数表示方式。JavaScript采用的是双精度浮点数表示法,也称为IEEE 754标准,它使用64位来表示一个数字,其中52位用于表示有效数字,而其他位用于表示符号、指数和特殊情况。

由于使用有限的位数来表示无限的小数,JavaScript无法准确地表示某些小数。其中一个典型的示例是0.1,它在二进制中是一个无限循环的小数。当我们将0.1这样的小数转换为二进制进行存储时,存在近似表示的误差,因此在进行计算时会出现精度丢失的问题。
例如,执行以下计算:

0.1 + 0.2

预期的结果应该是0.3,但在JavaScript中实际得到的结果是0.30000000000000004。这是因为0.1和0.2无法以精确的二进制形式表示,导致小数点精度丢失。

这种精度丢失是浮点数表示法的固有特性,并不仅限于JavaScript,其他编程语言也可能面临类似的问题。因此,在进行精确计算时,特别是涉及到货币、金融等方面的计算,应使用其它精确计算的方法,例如使用整数进行运算或使用专门的精确计算库。

总而言之,小数点精度丢失的问题是由于JavaScript使用的浮点数表示法的特性所致,需要在开发中注意,并考虑使用其他方法或工具来解决精度问题。

2、代码示例

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script>
        var floatObj = function () {

            // 判断传入的值-是否为整数
            function isInteger(obj) {
                return Math.floor(obj) === obj
            }

            // 将一个浮点数转成整数,返回整数和倍数。如 3.14 >> 314,倍数是 100
            // @param floatNum { number } 小数
            // @return { object }
            // { times: 100, num: 314 }

            // 用于返回整数和倍数
            function toInteger(floatNum) {
                // 声明一个对象用来保存倍数和整数
                var ret = { times: 1, num: 0 }
                // 第一种情况:是整数
                if (isInteger(floatNum)) {
                    // 把整数给 ret中的   num
                    ret.num = floatNum
                    return ret //  最后返回 ret
                }

                // 第二种情况-不是整数,
                var strfi = floatNum + ''  // 转为字符串  "0.1"
                var dotPos = strfi.indexOf('.')  // 查询小数点
                var len = strfi.substr(dotPos + 1).length; //  获取小数点后的长度
                var times = Math.pow(10, len)  // 放大多少倍
                var intNum = Number(floatNum.toString().replace('.', ''))  // 返回 转为字符串  截取掉小数点  最后转为数字(整数)
                // 把获取到的倍数和整数存入对象中
                ret.times = times
                ret.num = intNum
                return ret
            }


            // 核心方法,实现加减乘除运算,确保不丢失精度
            // 思路:把小数放大为整数(乘),进行算术运算,再缩小为小数(除)
            // @param a { number } 运算数1
            // @param b { number } 运算数2
            // @param digits { number } 精度,保留的小数点数,比如 2, 即保留为两位小数
            // @param op { string } 运算类型,有加减乘除(add / subtract / multiply / divide)



            function operation(a, b, digits, op) {
                // 获取倍数和整数的对象
                var o1 = toInteger(a)
                var o2 = toInteger(b)
                // 提取整数
                var n1 = o1.num
                var n2 = o2.num
                // 提取倍数
                var t1 = o1.times
                var t2 = o2.times

                // 获取最大倍数
                var max = t1 > t2 ? t1 : t2
                var result = null  //

                switch (op) {
                    case 'add':
                        if (t1 === t2) { // 两个小数位数相同
                            result = n1 + n2    //  
                        } else if (t1 > t2) { // o1 小数位 大于 o2
                            result = n1 + n2 * (t1 / t2)
                        } else { // o1 小数位 小于 o2
                            result = n1 * (t2 / t1) + n2
                        }
                        return result / max
                    case 'subtract':
                        if (t1 === t2) {
                            result = n1 - n2
                        } else if (t1 > t2) {
                            result = n1 - n2 * (t1 / t2)
                        } else {
                            result = n1 * (t2 / t1) - n2
                        }
                        return result / max
                    case 'multiply':
                        result = (n1 * n2) / (t1 * t2)
                        return result
                    case 'divide':
                        result = (n1 / n2) * (t2 / t1)
                        return result
                }
            }

            // 加减乘除的四个接口
            function add(a, b, digits) {
                return operation(a, b, digits, 'add')
            }
            function subtract(a, b, digits) {
                return operation(a, b, digits, 'subtract')
            }
            function multiply(a, b, digits) {
                return operation(a, b, digits, 'multiply')
            }
            function divide(a, b, digits) {
                return operation(a, b, digits, 'divide')
            }

            return {
                add: add,
                subtract: subtract,
                multiply: multiply,
                divide: divide
            }
        }();

        // console.log(floatObj.add(0.5, 0.2))
        console.log(floatObj.add(0.12, 0.3))



    </script>
</head>

<body>


</body>

</html>

 

三、解决思路

         解决方法:

         1.首先封装一个函数,这个函数是用来判断单价是否为一个整数。

         2.然后在封装第二个函数,这个函数的作用是返回整数和倍数的。

         同时声明一个对象ret来保存这个倍数和整数,
         第一种情况就是单价传过来时就是整数,那么他的倍数也就为1, 所以我们可以直接存储到声明的对象中
         第二种情况就是当单价传过来时是浮点数,这时候我们就要做处理,

        (1)、将传递过来的参数转为字符串(隐式转换),  
        (2)、使用indexOf找到小数点'.',
        (3)、使用substr字符串方法截取小数点后的长度length,
        (4)、声明一个为倍数的变量Time,同时使用Math.pow(10, 上一步截取的长度),   这个time就是倍数,小数点后每多一位就会为10×长度的次方,再将浮点数转换为整数(tostring转为字符, 使用replace将小数点替换为空,再用Number将字符串转为数字)
         最后将获取到的倍数和整数存入对象中即可

         3.最后实现加减乘除运算,确保不丢失精度
         主要思路:把小数放大为整数(乘),进行算术运算,再缩小为小数(除)
         再次封装一个为实现运算的函数这个函数接受三个参数(运算数1,运算数2, '加减乘除四个方法的函数'),
         声明n1, n2, 分别为单价1, 单价2,
         声明t1, t2.为单价1的倍数和单价2的倍数
         再获取最大倍数max(使用三元运算符),
         使用switch判断条件就是('加减乘除四个方法的函数')搭配Case判断

         加法: 三种状况
         1.t1和t2相同的情况下,就是两个小数的位数相同情况下直接返回n1 + n2 就是整数相加
         2.t1大于t2的情况下 第一个小数位数大于第二位返回, n1 + n2 * (t1 / t2):
         3.t1小于t2的情况下 第一个小数位数小于第二位返回, n1 * (t2 / t1) + n2
         最后无论走得是哪一种情况结果都要除以max(最大倍数)

         减法: 三种状况
         1.t1和t2相同的情况下,就是两个小数的位数相同情况下直接返回n1 - n2 就是整数相减
         2.t1大于t2的情况下 第一个小数位数大于第二位返回, n1 - n2 * (t1 / t2)
         3.t1小于t2的情况下 第一个小数位数小于第二位返回, n1 * (t2 / t1) - n2
         最后无论走得是哪一种情况结果都要除以max(最大倍数)

         乘法: 判断状态时候结果的是(n1 × n2)/ (t1 × t2)

         除法: 判断状态时候结果的是(n1 × n2)/ (t1 × t2)

         --- 最后进行加减乘除进行封装,return这四个函数-- -

四、解决的问题

在 JavaScript 中处理小数点精度可以带来以下几个好处:

避免精度丢失:JavaScript 使用 IEEE 754 浮点数标准来表示数字,但由于浮点数存储的是二进制近似值,会导致一些小数无法被准确表示,从而造成精度丢失。通过处理小数点精度,可以减少这种精度丢失的情况,确保计算结果的准确性。

精确计算金融数据:在金融领域,小数点精度非常重要。处理货币金额、计算利率和利息等都需要保持精确的小数点计算,以避免数据错误和损失。

避免舍入误差:当进行多个浮点数计算时,由于每次计算都可能存在一定的舍入误差,累积计算结果可能会与预期的结果有所偏差。通过处理小数点精度,可以减少舍入误差的影响,提高计算的准确性。

增强数据可视化:在图表和可视化数据中,小数点精度可以提供更精确的数据展示。例如,在绘制股票价格曲线或者科学实验数据时,小数点精度可以使得数据更加准确和可信。

总的来说,处理小数点精度可以确保计算结果的准确性,尤其在对于金融数据和需要高精度计算的场景下,这种处理是非常重要的。

五、二进制存储原理

在JS中不区分整数和小数,因为JS中天生浮点数(双精度),在计算机存储中,双精度的实际存储位数是52位,由于二进制中只有 0 和 1,但52位有时并不能准确的表达小数点后面的数字,在十进制中有四舍五入,在二进制中存在0舍1入,所以当52位无法准确的表达出一个小数时,就会产生补位动作,数值偏差就在这时产生了,这是造成计算精度问题的原因。