js的精度问题
# js中存在的精度问题及解决方案
# 一、js中问什么会存在精度问题:为什么0.1+0.2 != 0.3?
当将十进制数转换为二进制数时,0.1 和 0.2 都会产生无限循环的二进制表示形式。
这是因为在二进制中,有些小数无法精确表示,就像在十进制中无法精确表示 1/3 一样。
将 0.1 转换为二进制:
乘以 2,得到 0.2,整数部分为 0。
乘以 2,得到 0.4,整数部分为 0。
乘以 2,得到 0.8,整数部分为 0。
乘以 2,得到 1.6,整数部分为 1。
乘以 2,得到 1.2,整数部分为 1。
乘以 2,得到 0.4,整数部分为 0。
乘以 2,得到 0.8,整数部分为 0。
乘以 2,得到 1.6,整数部分为 1。
乘以 2,得到 1.2,整数部分为 1。
继续该过程下去,得到无限循环的二进制表示形式:0.0001100110011...
将 0.2 转换为二进制:
乘以 2,得到 0.4,整数部分为 0。
乘以 2,得到 0.8,整数部分为 0。
乘以 2,得到 1.6,整数部分为 1。
乘以 2,得到 1.2,整数部分为 1。
继续该过程下去,得到无限循环的二进制表示形式:0.001100110011...
由于这种无限循环,将它们精确表示为二进制是不可能的.
在计算机中,它们的二进制表示会被截断为有限位数,从而引入舍入误差。
因此,在实际计算中,0.1 和 0.2 的精确值不是直接使用它们的二进制表示,而是使用近似值进行计算。 这导致了 0.1 + 0.2 的结果略微偏离于 0.3。
# 二、关于精度的解决方案
1、使用专门的高精度计算库:可以使用第三方库或语言内置的高精度计算库,这些库提供了更精确的数值计算功能。
例如,在 JavaScript 中,可以使用 Decimal.js、Big.js、math.js 等库来进行高精度计算。
2、使用字符串表示数字实现精确计算:如果不想依赖外部库,也可以手动实现高精度计算。 一种常见的方法是使用字符串表示数字,然后编写相应的算法来进行精确计算。通过使用字符串表示,可以绕过浮点数的精度问题。 但是,这样做可能会降低计算速度和增加代码复杂性。
```javascript // 将数字转换为字符串 function numberToString(num) { return num.toString(); }
// 去除字符串首尾的0 function trimLeadingAndTrailingZeros(str) { return str.replace(/^0+|0+$/g, ''); }
// 加法 function add(a, b) { const aStr = numberToString(a); const bStr = numberToString(b);
let carry = 0; // 进位 let result = ''; // 结果
let i = aStr.length - 1; // 数字 a 的最后一位索引 let j = bStr.length - 1; // 数字 b 的最后一位索引
// 从后往前遍历 a 和 b 的每一位数字,进行相加 while (i >= 0 || j >= 0 || carry > 0) { const digitA = i >= 0 ? parseInt(aStr[i]) : 0; // 数字 a 的当前位数值,若索引越界则为0 const digitB = j >= 0 ? parseInt(bStr[j]) : 0; // 数字 b 的当前位数值,若索引越界则为0
// 计算当前位的和,加上进位
const sum = digitA + digitB + carry;
// 更新进位
carry = Math.floor(sum / 10);
// 将当前位的结果添加到结果字符串的开头
result = (sum % 10) + result;
// 移动到下一位
i--;
j--;
}
// 去除首尾无效的0,并返回结果 return trimLeadingAndTrailingZeros(result); }
// 乘法 function multiply(a, b) { const aStr = numberToString(a); const bStr = numberToString(b);
const products = []; // 存储各位乘积的临时数组
// 从 a 的最后一位开始遍历 for (let i = aStr.length - 1; i >= 0; i--) { let carry = 0; // 进位 let product = ''; // 当前位的乘积
// 从 b 的最后一位开始遍历
for (let j = bStr.length - 1; j >= 0; j--) {
const digitA = parseInt(aStr[i]); // 数字 a 的当前位数值
const digitB = parseInt(bStr[j]); // 数字 b 的当前位数值
// 计算当前位的乘积,加上进位
const partialProduct = digitA * digitB + carry;
// 更新进位
carry = Math.floor(partialProduct / 10);
// 将当前位的结果添加到当前乘积的开头
product = (partialProduct % 10) + product;
}
// 若进位大于0,则将其添加到当前乘积的开头
if (carry > 0) {
product = carry + product;
}
// 添加0,相当于当前位乘积的位数向左移动
product += '0'.repeat(aStr.length - i - 1);
// 将当前位的乘积添加到临时数组中
products.push(product);
}
// 将各位乘积相加,得到最终结果 let result = '0'; for (const product of products) { result = add(result, product); }
// 去除首尾无效的0,并返回结果 return trimLeadingAndTrailingZeros(result); }
// 示例 const a = '0.1'; const b = '0.2'; const sum = add(a, b); console.log(sum); // 输出:"0.3"
const product = multiply(a, b); console.log(product); // 输出:"0.02"
[//]: # (3、调整计算顺序:有时,通过重新排列计算顺序可以减少舍入误差的影响。)
[//]: # ( 例如,如果某个计算顺序导致大数与小数相加,可以尽量将相似大小的数值进行相加,然后再进行舍入。这样可以减小舍入误差的累积效应。)
<br/><br/>
3、**使用整数计算**:对于需要高精度的货币计算等场景,可以将小数转换为整数,进行整数计算,然后再将结果转换回小数形式。
这可以避免浮点数精度问题,因为整数计算没有舍入误差。
<br/>
```javascript
function decimalToIntegerCalculation(a, b, operator) {
// 将小数转换成整数
const precision = Math.max(getPrecision(a), getPrecision(b));
const integerA = a * Math.pow(10, precision);
const integerB = b * Math.pow(10, precision);
// 进行整数运算
let result;
switch (operator) {
case '+':
result = integerA + integerB;
break;
case '-':
result = integerA - integerB;
break;
case '*':
result = (integerA * integerB) / Math.pow(10, precision * 2);
break;
case '/':
result = integerA / integerB;
break;
default:
throw new Error("Invalid operator");
}
// 将结果转回小数形式
return result / Math.pow(10, precision);
}
// 获取小数的精度
function getPrecision(number) {
const str = number.toString();
const decimalIndex = str.indexOf(".");
return decimalIndex === -1 ? 0 : str.length - decimalIndex - 1;
}
// 示例
const a = 0.1;
const b = 0.2;
const sum = decimalToIntegerCalculation(a, b, '+');
console.log(sum); // Output: 0.3