解码为什么 JS 中的 0.6 + 0.3 = 0.89999999999999 以及如何解决?

在 Hacktoberfest 期间,我在一个开源计算器软件仓库工作时,发现某些小数计算没有产生预期的结果,比如 0.6+0.3 的结果不会是 0.9,于是我在想是不是代码出了问题。但进一步分析后发现,这是 JavaScript 的实际行为。于是深入研究,了解其内部工作原理。

在这篇博文中,我将与大家分享我的见解,并讨论几种解决方法。

在日常数学中,我们知道 0.6 + 0.3 相加等于 0.9,对吗?但当我们使用计算机时,结果却是 0.89999999999999。令人惊讶的是,这种情况不仅发生在 JavaScript 中,在 Python、Java 和 C 等许多编程语言中也同样存在。此外,这不仅仅是这个特定的计算。还有很多十进制计算也会出现类似的错误答案。

为什么会出现这种情况?

这与计算机如何处理浮点数有关。十进制数使用一种名为 IEEE 754 标准的格式存储在计算机内存中。IEEE 754 浮点标准是当今计算机中最常用的实数表示法。该标准包括不同的表示类型,主要是单精度(32 位)和双精度(64 位)。JavaScript 遵循 IEEE 754 双精度浮点标准。

双精度由 64 位组成,包括 1 个符号位、11 个指数位和 52 个尾数(小数部分)位。
任何十进制数都只能以这种双精度 IEEE 754 二进制浮点格式存储。计算机系统中的有限 64 位表示法无法准确表达所有十进制数值,尤其是那些具有无限十进制扩展的数值,这导致在处理某些二进制数时,结果会出现细微差异。

让我们通过一个例子来了解十进制数的存储方式,并揭示为什么 0.6+0.3 等于 0.89999999999999
用 IEEE 754 双精度浮点格式表示 0.6

# 第 1 步:将十进制 (0.6)₁₀ 转换为二进制表示base 2

整数部分0/2 = 0

小数部分:
重复乘以 2,注意结果的每个整数部分,直到小数部分为零。

0.1 不能精确地表示为二进制分数。高亮部分无休止地重复出现,形成一个无穷序列。此外,我们也没有得到任何零分数部分。

# 第 2 步:归一化

如果小数点左边有一个非零的数字,那么用科学计数法写出的数字 x 就被归一化了,也就是说,强制其尾数的整数部分正好为 1。

我们根据 IEEE 标准的要求调整我们的序列,包括 52 位尾数和四舍五入的有限数量。这就是出现舍入错误的原因。

#步骤 3:调整指数:

对于双精度,-1022 至 +1023 范围内的指数偏差通过添加 1023 来获得。

exponent => -1 + 1023 => 1022 用 11 位二进制表示数值。

(1022)₁₀ => (010111111110)₂

符号位为 0,因为 0.6 是正数。(-1)⁰=> 1
现在,我们可以用 IEEE 754 浮点格式表示所有数值。

在对尾数进行规范化处理的过程中,前导位(最左侧)1 将被删除,因为它始终为 1,只有在必要时(此处并非如此)才将其长度调整为 52 位。

同样,用同样的方法,0.3 可以表示为

# 将两个值相加

1.平衡指数

由于我们有 0.60.3 这两个值,因此必须将它们相加。但在此之前,要确保指数相同。在这种情况下,它们不相等。因此,我们需要调整它们,将较小的指数与较大的数值相匹配。

0.6 的指数=>-1 0.3 的指数=>-2,我们必须将 0.30.6 配对,因为 0.6 的指数大于 0.3

这里的差值为 1,因此 0.3 的尾数需要右移 1 位,指数代码增加 1 以匹配 0.6。

将尾数移动 1 位会导致最小有效位丢失,以保持 64 位标准,这可能会带来精度误差。

2.尾数加法

由于现在指数相等,我们需要对尾数进行二进制加法。

现在的值将是 0; 01111111110; 1.11001100110011001100110011001100110011001100110011001100110011001100

3.对得到的尾数进行归一化和四舍五入

在本例中,尾数已经归一化 [前导位为 1],因此跳过这一步。

最后,0.6+0.3 的结果表示为

因此,现在我们得到的结果是 0.6 + 0.3,它以 64 位 IEEE 格式表示,机器可读。我们必须将其转换回十进制,以便于人类阅读。

#将 IEEE 754 浮点表示法转换为十进制等价形式

确定符号位: 0 => (-1)⁰ => +1

计算无偏指数:

(01111111110)₂ => (1022)₁₀

2^(e-1023) => 2^(1022-1023) => 2^-1

分数部分:

从尾数最左边的一位开始,将每个位的值相加,然后乘以 2 的幂。1×2^-1 + 1×2^-2 + 0×2^-3 + .......+ 0×2^-52

将这些数值代入方程并求解,得到 0.89999999999999 的结果,并显示在控制台中。[四舍五入]

=> +1 (1+ 1×2^-1 + 1×2⁻^-2 + 0×2^-3 + ....... + 0×2^-52) x 2^-1
=> +1 (1 + 0.79999999999999982236431605997495353221893310546875) x 2^-1
=> 0.8999999999999999111821580299874767661094665527343750.8999999999999999 //numbers are rounded

//Because floating-point numbers have a limited number of digits,
//they cannot represent all real numbers accurately: when there 
//are more digits, the leftover ones are omitted,i.e. the number is rounded

让我们再举一个例子,加深对著名表达式 0.1 + 0.2 = 0.30000000000000004 的理解。

添加 64 位 IEEE 754 二进制浮点数值 0.1 和 0.2

将结果转换回十进制

如何解决?

让我们来看看在处理货币或金融计算的应用程序时,如何获得精确的结果,因为精确度是至关重要的。

i) 内置函数:toFixed() toPrecision()

  • toFixed() 将数字转换为字符串,并将字符串四舍五入为指定的小数位数。
  • toPrecision()将数字格式化为特定精度或长度,并根据需要添加尾数零以达到指定精度。 parseFloat()用于删除数字的尾数零。
const num1 = 0.6;
const num2 = 0.3;
const result = num1 + num2;

const toFixed = result.toFixed(1); 
const toPrecision = parseFloat(result.toPrecision(12));

console.log("Using toFixed(): " + toFixed); // Output: 0.9
console.log("Using toPrecision(): " + toPrecision); // Output: 0.9

限制

toFixed() 总是将数字四舍五入到给定的小数位,这可能不会在所有情况下都一致。 toPrecision() 也类似,但对于非常小或非常大的数字,它可能不会产生准确的结果,因为它的参数应该在 1-100 之间。

//1. Adding 0.03 and 0.255 => expected 0.283
console.log((0.03 + 0.253).toFixed(1)) // returns 0.3

//2. Values are added as a string
(0.1).toPrecision()+(0.2).toPrecision() // returns 0.10.2

ii) 第三方库

有各种库(如 math.jsdecimal.jsbig.js)可以解决这个问题。每个库都根据其文档发挥作用。这种方法相对更好。

//Example using big.js
const Big = require('big.js');

Big.PE = 1e6; // Set positive exponent for maximum precision in Big.js

console.log(new Big(0.1).plus(new Big(0.2)).toString());    //0.3
console.log(new Big(0.6).plus(new Big(0.3)).toString());    //0.9
console.log(new Big(0.03).plus(new Big(0.253)).toString()); //0.283
console.log(new Big(0.1).times(new Big(0.4)).toString());   //0.04

结论

用于存储十进制数的 IEEE 754 标准可能会导致微小的差异。可以使用各种库来获得更精确的结果。根据应用需求选择合适的方法。其他语言中也有类似的软件包,如 Java 的 BigDecimal 和 python 的 Decimal

本文文字及图片出自 Decoding Why 0.6 + 0.3 = 0.8999999999999999 in JS and How to Solve?

余下全文(1/3)
分享这篇文章:

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注