js 丢失数学精度的原因
JavaScript 中的浮点数使用 IEEE 754 标准来表示,这种表示方式使用有限的位数来存储实数,因此存在精度限制。这就是为什么在某些情况下,对浮点数进行数学运算会导致精度丢失的原因。
主要的原因包括:
二进制表示:浮点数在计算机内部以二进制表示。某些小数在二进制下是无限循环的,就像 1/3 在十进制下是 0.3333... 一样。这意味着某些小数不能以精确的方式表示为浮点数。
有限位数:浮点数通常使用 32 位或 64 位表示,其中一部分用于表示整数部分,一部分用于表示小数部分。由于位数有限,某些小数会被截断,导致精度丢失。
舍入误差:在进行浮点数运算时,舍入误差可能会累积,导致结果不精确。这是因为计算机内部表示的位数有限,不能容纳所有的小数位数。
计算顺序:浮点数运算的计算顺序可能会影响结果的精度。某些运算可能会导致更大的精度损失,而某些运算可能会减小精度损失。
为了处理精确数学运算,可以使用 bigint
类型来表示整数,或者使用专门的精确数学库,如 Decimal.js
。这些方法可以提供更高的精度和控制,但可能会导致性能损失。
1.IEEE 754 国际标准
IEEE 754 是一个国际标准,定义了浮点数表示和运算的规范。在 JavaScript 和许多其他编程语言中,双精度浮点数是一种常用的浮点数表示方式,符合 IEEE 754 标准。以下是关于双精度浮点数的一些重要信息:
二进制表示:双精度浮点数使用二进制来表示实数。一个典型的双精度浮点数包括一个符号位、一个指数部分和一个尾数(或称为尾数部分)。指数部分用来表示数的次方,尾数部分用来表示数的有效位数。
位数分配:双精度浮点数总共占据 64 位(8 字节)的存储空间,通常被分配如下:
- 1 位用于符号位(表示正数或负数)。
- 11 位用于指数部分,指数部分可以表示数的范围。
- 52 位用于尾数部分,尾数部分用于表示数值的精度。
有限精度:由于双精度浮点数的尾数部分只有 52 位,因此无法表示所有的小数,尤其是无限循环小数。这导致了精度损失,一些小数在二进制浮点表示中无法精确表示。
如果尾数全部表示为整数,则为最大
2^53-1(9007_1992_5474_0991)
,大于2^53-1(9007_1992_5474_0991)
就是丢失精度。需要整数精度只能用 bigint
- 舍入误差:在进行浮点数运算时,舍入误差可能会累积,导致最终结果的精度不准确。这是因为某些小数在二进制中无法以精确方式表示,因此它们的近似值用于计算。
对于 0.1
和 0.2
来说,它们在二进制浮点表示中是无限循环小数,无法以精确方式表示。当你执行 0.1 + 0.2
时,JavaScript 使用最接近的双精度浮点数表示,但这个结果并不等于精确的 0.3
,因此会产生一个非常小的舍入误差,通常以 0.00000000000000004
这样的形式出现。
这就是为什么 0.1 + 0.2
在 JavaScript 中得到 0.30000000000000004
的原因。要进行精确的小数计算,需要使用其他方法,如使用整数来表示小数或使用精确数学库。
2. 浮点数转二进制
将普通的十进制浮点数转化为二进制小数涉及到一些特定的规则和步骤。这个过程被称为浮点数的规范化。
以下是将十进制浮点数转化为二进制小数的一般步骤:
将整数部分和小数部分分开。
将整数部分转化为二进制整数。这可以通过反复除以 2 并记录余数的方式来完成,直到整数部分为 0。将记录的余数反向排列,得到二进制整数部分。
将小数部分转化为二进制小数。这可以通过反复乘以 2 并取整数部分的方式来完成。将整数部分记录下来,并将小数部分重复上述步骤,直到小数部分为 0 或者达到所需的精度。
组合整数部分和小数部分,用小数点分隔。
这个过程有时候可能会产生无限循环的二进制小数,这就需要舍入到所需的精度。在计算机中,浮点数通常使用 IEEE 754 标准来表示,其中包括规范化浮点数的方式,以及指数部分和尾数部分的表示。
要注意的是,一些十进制小数无法精确表示为有限位数的二进制小数,就像 0.1
在二进制中是无限循环的。因此,浮点数表示会涉及到近似值,可能会有精度损失。在实际编程中,通常使用编程语言提供的浮点数表示,而不需要手动进行这个转化。如果需要更高精度的小数表示,可以使用 BigDecimal 或其他精确数学库。
3. 小数转二进制
一个示例来说明如何将十进制浮点数转化为二进制小数。我们将以十进制数 0.1
为例:
- 整数部分为
0
。 - 小数部分为
1
。
现在,我们开始将小数部分转化为二进制小数:
- 小数部分
1
乘以2
得到2
,整数部分为1
,小数部分为0.0
。 - 小数部分
0.0
乘以2
得到0
,整数部分为1
,小数部分为0.00
。 - 小数部分
0.00
乘以2
得到0
,整数部分为1
,小数部分为0.000
。
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.4*2=0.8 ---------0
0.8*2=1.6 ---------1
0.6*2=1.2 ---------1
// 结果依次排列为:
// 0.0 0011 0011 0011 0011...
我们可以看到,小数部分一直都是 0
,这意味着 0.1
的二进制表示是一个有限的二进制小数。因此,0.1
的十进制浮点数表示为 0.00011001100110011...
,以及一直重复下去。
但是要注意,双精度浮点数表示在一定位数之后会进行截断,因此在实际计算中,0.1
在 JavaScript 中表示为一个近似值,通常以 0.1000000000000000055511151231257827021181583404541015625
这样的形式出现。这是因为双精度浮点数无法精确表示 0.1
这个无限循环小数。
将整数转化为二进制通常涉及到不断地除以 2 并记录余数的过程,直到商为 0 为止。以下是将整数转化为二进制的一般步骤:
选择要转化的整数。
不断地将这个整数除以 2,同时记录每一步的余数和商。余数只能是 0 或 1,因为这是二进制。
将记录的余数反向排列,得到二进制表示的整数。
4. 整数转二进制
我们将把十进制数 37
转化为二进制:
1. 37 / 2 = 18 余 1
2. 18 / 2 = 9 余 0
3. 9 / 2 = 4 余 1
4. 4 / 2 = 2 余 0
5. 2 / 2 = 1 余 0
6. 1 / 2 = 0 余 1
// 结果取反向:100101
现在,我们将记录的余数反向排列,得到二进制表示的整数:
37 的二进制表示为 100101
所以,十进制数 37
在二进制中表示为 100101
。
在实际编程中,大多数编程语言提供了内置的方法来将整数转化为二进制字符串,因此你通常不需要手动进行这个过程。例如,在 JavaScript 中,可以使用 Number.toString()
方法来将整数转化为二进制字符串:
const decimalNumber = 37;
const binaryString = decimalNumber.toString(2);
console.log(binaryString); // 输出: "100101"
这个方法非常方便,可以直接得到整数的二进制表示。
5.使用 Decimal.js 进行数学计算
Decimal.js
是一个 JavaScript 库,用于进行高精度的十进制数学运算。它是一个用于精确数字计算的库,特别适用于处理需要高精度计算的应用程序,例如金融计算、科学计算和货币处理等领域。
以下是一些 Decimal.js
的特点和示例用法:
高精度计算:
Decimal.js
允许你进行高精度的十进制计算,避免了普通浮点数精度问题。支持链式调用:你可以通过链式调用方法来进行一系列的计算操作。
支持各种运算:它支持加法、减法、乘法、除法、取余、取整等各种基本运算,以及其他高级数学函数。
丰富的配置选项:你可以配置数字的精度、舍入方式、舍入位数等。
以下是一个简单的示例,演示如何在 JavaScript 中使用 Decimal.js
进行高精度计算:
首先,你需要安装 decimal.js
包(或使用其他适合你的方式安装):
npm install decimal.js
然后,你可以在代码中引入并使用它:
const Decimal = require("decimal.js");
// 创建 Decimal 对象
const num1 = new Decimal("0.1");
const num2 = new Decimal("0.2");
// 进行高精度加法运算
const result = num1.plus(num2);
console.log(result.toString()); // 输出: "0.3"
在这个示例中,我们使用 Decimal.js
创建了两个精确的十进制数字,然后使用 .plus()
方法进行加法运算,得到了精确的结果。
Decimal.js
提供了丰富的功能,允许你执行各种高精度的数学运算,以确保数值计算的精度和准确性。