Skip to content

JavaScript数字精度问题

前言

在JavaScript中,浮点数计算经常会出现精度问题,比如0.1 + 0.2 !== 0.3。本文将深入解析这个问题的原因,并提供多种实用的解决方案。

一、问题展示

1.1 典型案例

javascript
// 最常见的精度问题
console.log(0.1 + 0.2);        // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false

// 其他常见案例
console.log(0.7 + 0.1);        // 0.7999999999999999
console.log(0.2 + 0.4);        // 0.6000000000000001

1.2 实际业务场景

javascript
// 金额计算
const price = 0.1;
const quantity = 3;
console.log(price * quantity);  // 0.30000000000000004

// 折扣计算
const discount = 0.8;
const total = 100;
console.log(total * discount);  // 79.99999999999999

二、原理解析

2.1 IEEE 754标准

JavaScript使用IEEE 754双精度浮点数格式,用64位存储数字:

┌─────────────┬──────────────────┬─────────────────────────┐
│  符号位(1位) │   指数位(11位)   │      尾数位(52位)       │
└─────────────┴──────────────────┴─────────────────────────┘

2.2 二进制转换过程

javascript
// 以0.1为例
0.1 -> 二进制
0.1 * 2 = 0.2  -> 0
0.2 * 2 = 0.4  -> 0
0.4 * 2 = 0.8  -> 0
0.8 * 2 = 1.6  -> 1
0.6 * 2 = 1.2  -> 1
0.2 * 2 = 0.4  -> 0
// ... 循环

// 最终0.1的二进制表示为:
0.0001100110011001100110011001100110011001100110011001101

三、解决方案

3.1 四舍五入法

javascript
// 使用toFixed()
function roundTo(num, precision) {
    return Number(Math.round(num + 'e' + precision) + 'e-' + precision);
}

console.log(roundTo(0.1 + 0.2, 2)); // 0.3

3.2 转换为整数计算

javascript
class NumberUtil {
    // 加法
    static add(num1, num2) {
        const baseNum = Math.pow(10, Math.max(
            this.getDecimals(num1),
            this.getDecimals(num2)
        ));
        return (num1 * baseNum + num2 * baseNum) / baseNum;
    }
    
    // 获取小数位数
    static getDecimals(num) {
        const decimal = String(num).split('.')[1];
        return decimal ? decimal.length : 0;
    }
    
    // 乘法
    static multiply(num1, num2) {
        const baseNum = Math.pow(10, 
            this.getDecimals(num1) + this.getDecimals(num2)
        );
        return (num1 * baseNum) * (num2 * baseNum) / (baseNum * baseNum);
    }
}

// 使用示例
console.log(NumberUtil.add(0.1, 0.2));      // 0.3
console.log(NumberUtil.multiply(0.1, 0.2));  // 0.02

3.3 使用第三方库

javascript
// 使用big.js
import Big from 'big.js';

const x = new Big(0.1);
const y = new Big(0.2);
console.log(x.plus(y).toString());  // "0.3"

// 使用decimal.js
import Decimal from 'decimal.js';

const a = new Decimal(0.1);
const b = new Decimal(0.2);
console.log(a.plus(b).toString());  // "0.3"

四、最佳实践

4.1 金融计算

javascript
class FinanceCalculator {
    static formatMoney(amount) {
        return new Big(amount)
            .round(2)
            .toFixed(2);
    }
    
    static calculateDiscount(price, discount) {
        return new Big(price)
            .times(discount)
            .round(2)
            .toNumber();
    }
}

// 使用示例
console.log(FinanceCalculator.formatMoney(0.1 + 0.2));        // "0.30"
console.log(FinanceCalculator.calculateDiscount(100, 0.8));   // 80.00

4.2 性能优化

javascript
// 缓存Big实例
const cache = new Map();

function getCachedBig(num) {
    const key = String(num);
    if (!cache.has(key)) {
        cache.set(key, new Big(num));
    }
    return cache.get(key);
}

总结

  1. 理解精度问题的根源:IEEE 754标准的限制
  2. 根据场景选择合适的解决方案:
    • 简单计算:四舍五入
    • 精确计算:整数转换
    • 复杂场景:第三方库
  3. 在金融计算中始终使用专业的数学库
  4. 注意性能优化,合理使用缓存

记住:在处理金融数据时,精度问题不容忽视!