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位无法准确的表达出一个小数时,就会产生补位动作,数值偏差就在这时产生了,这是造成计算精度问题的原因。