js 浮点数运算精度问题

73 阅读2分钟

问题描述

在 JavaScript 中整数和浮点数都属于 Number 类型,而在浮点数做运算的时候经常会出现一些精度问题,以下例子:

console.log(0.1 + 0.2);  // 0.30000000000000004
console.log(0.2 + 0.4);  // 0.6000000000000001
console.log(0.1 + 0.7);  // 0.7999999999999999
console.log(2.22 + 0.1); // 2.3200000000000003

console.log(0.3 - 0.2);  // 0.09999999999999998
console.log(1.5 - 1.2);  // 0.30000000000000004
console.log(2.3 - 2);    // 0.2999999999999998

console.log(0.3 * 3);    // 0.8999999999999999
console.log(0.57 * 7);   // 3.9899999999999998
console.log(19.9 * 100); // 1989.9999999999998
console.log(35.7 * 100); // 3570.0000000000005

console.log(0.3 / 0.1);  // 2.9999999999999996
console.log(0.7 / 10);   // 0.06999999999999999

问题原因

不仅仅是 JavaScript,所有支持二进制浮点数运算的系统都存在这个问题。

其原因是,JavaScript 的 Number 类型是一个双精度 64 位二进制格式 IEEE 754 值,类似于 Java 或者 C# 中的 double 。IEEE 754 双精度浮点数使用 64 位来表示 3 个部分:

  • 第 0 位:符号位,0 表示正数,1 表示负数
  • 第 1 位到第 11 位:表示指数(-1022 到 1023)
  • 第 12 位到第 63 位:表示尾数(有效数字)的数值部分(表示 0 和 1 之间的数值)

尾数(也称为有效数)是表示实际值(有效数字)的数值部分。指数是尾数应乘以的 2 的幂次。

尾数使用 52 比特存储,在二进制小数中解释为 1.… 之后的数字。因此,尾数的精度是 2522^{-52}(可以通过 Number.EPSILON 获得),或者十进制数小数点后大约 15 到 17 位;超过这个精度的算术会受到舍入的影响。

一个数值可以容纳的最大值是 21024 12^{1024} - 1 (指数为 1023,尾数为基于二进制的 0.1111…),可以通过 Number.MAX_VALUE 获得。超过这个值的数会被替换为特殊的数值常量 Infinity

只有在 253 +1-2^{53} + 1253 12^{53} - 1 范围内(闭区间)的整数才能在不丢失精度的情况下被表示(可通过 Number.MIN_SAFE_INTEGER 和 Number.MAX_SAFE_INTEGER 获得),因为尾数只能容纳 53 位(包括前导 1)。

更多详细信息,请参阅 ECMAScript 标准

解决办法

1. 第三方库

大量的、精确的计算建议交给后端处理再返回。以下几个成熟的前端类库也可供使用:

Math.js 是 JavaScript 和 Node.js 的广泛数学库。它具有灵活的表达式解析器,并支持符号计算,并带有大量内置功能和常数,并提供了一种集成解决方案来处理不同的数据类型,像数字,大数字,复数,分数,单位和矩阵。

用于任意精确算术的 JavaScript 库。

为 JavaScript 提供十进制类型的任意精度数值。

2. toFixed()

使用 toFixed 缩小精度再使用 parseFloat 转成数字:

function strip(num, precision = 12) {
  return +parseFloat(num.toFixed(precision));
}

Number 类型的 toFixed() 方法返回一个使用定点表示法来格式化该数值的字符串。

console.log(strip(0.1 + 0.2));  // 0.3
console.log(strip(0.2 + 0.4));  // 0.6
console.log(strip(0.1 + 0.7));  // 0.8
console.log(strip(2.22 + 0.1)); // 2.32

console.log(strip(0.3 - 0.2));  // 0.1
console.log(strip(1.5 - 1.2));  // 0.3
console.log(strip(2.3 - 2));    // 0.3

console.log(strip(0.3 * 3));    // 0.9
console.log(strip(0.57 * 7));   // 3.99
console.log(strip(19.9 * 100)); // 1990
console.log(strip(35.7 * 100)); // 3570

console.log(strip(0.3 / 0.1));  // 3
console.log(strip(0.7 / 10));   // 0.07