先乘后除 与 先除后乘

4 阅读5分钟

在 JavaScript 中,先乘后除和先除后乘的结果可能不同,这主要是由于两个原因:

  1. 运算顺序:在数学中,乘法和除法是同级运算,按照从左到右的顺序进行计算。在 JavaScript 中,乘法和除法的运算顺序也是从左到右。

  2. 精度问题:在计算机中,浮点数的表示和计算存在精度问题,这可能导致乘法和除法的结果出现微小的差异。

    let result3 = 0.1 * 0.2 / 0.3;
    console.log(result3); // 0.06666666666666667
    

    在这个例子中,先计算 0.1 * 0.2 得到 0.02,然后 0.02 / 0.3 得到 0.06666666666666667

    let result4 = 0.1 / 0.3 * 0.2;
    console.log(result4); // 0.06666666666666666
    

    在这个例子中,先计算 0.1 / 0.3 得到 0.3333333333333333,然后 0.3333333333333333 * 0.2 得到 0.06666666666666666

顺序问题非常基础和明白,所以接下来我们主要谈谈“精度问题”

精度问题

精度问题主要源于浮点数的表示方式。浮点数在计算机中是以二进制形式存储的,这种表示方式无法精确地表示某些十进制小数。因此,在进行浮点数运算时,可能会出现微小的误差。这种现象在几乎所有编程语言中都存在,因为它们都基于 IEEE 754 标准来表示浮点数。

IEEE 754 是一种浮点数表示标准,用于规范计算机中的浮点数运算。JavaScript 中的浮点数遵循 IEEE 754 的双精度(64位)格式。这种格式可以表示非常大的范围,但牺牲了精度,尤其是对于某些十进制小数。 浮点数的二进制表示由三部分组成:

  1. 符号位(Sign Bit):1 位,表示数字的正负。
  2. 指数位(Exponent):11 位,表示数字的大小范围。
  3. 尾数位(Mantissa/Fraction):52 位,表示数字的精度。 这种表示方式使得浮点数可以表示非常大或非常小的数字,但某些十进制小数无法精确表示为二进制小数。例如,0.1 在二进制中是一个无限循环小数,类似于十进制中的 1/3

为什么 0.1+0.2 不等于 0.3

因为并不是所有的十进制小数都能精确地转换为二进制小数。0.1 就是这样一个例子,它无法被精确地表示为二进制浮点数。以下是详细解释:

0.1 转换为二进制小数:

1.  `0.1 * 2 = 0.2`,取整数部分 `0`2.  `0.2 * 2 = 0.4`,取整数部分 `0`3.  `0.4 * 2 = 0.8`,取整数部分 `0`4.  `0.8 * 2 = 1.6`,取整数部分 `1`5.  `0.6 * 2 = 1.2`,取整数部分 `1`6.  `0.2 * 2 = 0.4`,取整数部分 `0`7.  `0.4 * 2 = 0.8`,取整数部分 `0`8.  `0.8 * 2 = 1.6`,取整数部分 `1`9.  `0.6 * 2 = 1.2`,取整数部分 `1`10.  `0.2 * 2 = 0.4`,取整数部分 `0`

这个过程会无限循环下去,因为 0.1 在二进制中是一个无限循环小数,类似于十进制中的 1/3。因此,0.1 的二进制表示是 0.00011001100110011...,这是一个无限循环小数。

由于 0.1 的二进制表示是无限循环的,而浮点数的尾数部分只有 52 位,因此无法精确表示 0.1。在存储时,0.1 会被截断为一个近似值,这个近似值在十进制中是 0.1000000000000000055511151231257827021181583404541015625

由于 0.1 无法精确表示,涉及 0.1 的运算也可能会产生精度误差。所以 0.1+0.2 不等于 0.3

解决办法

1 使用 toFixed() 或 `Math.round()

可以通过 toFixed()Math.round() 来限制小数位数,减少误差的影响。

let result = (0.1 + 0.2 + 0.3) * 10;
console.log(result.toFixed(2)); // 输出:"6.00"

2 使用整数运算(先乘后除) 尽量将浮点数运算转换为整数运算,避免浮点数精度问题。

let result = (0.1*100 + 0.2*100) / 100; // 将浮点数转换为整数
console.log(result); // 输出:3

3 使用第三方库

对于需要高精度运算的场景,可以使用第三方库,如 decimal.jsbignumber.js

const Decimal = require("decimal.js");
let result = new Decimal(0.1).plus(0.2).plus(0.3).mul(10).toNumber();
console.log(result); // 输出:6

二进制转换

将十进制数转换为二进制数是计算机科学中的一个基本操作。这个过程涉及到将十进制数的整数部分和小数部分分别转换为二进制形式。以下是详细的转换步骤和示例。

  • 十进制整数转二进制整数

    1. 除以2取余:将十进制整数除以2,记录余数。
    2. 重复操作:将商继续除以2,记录余数,直到商为0。
    3. 读取余数:将记录的余数从下到上读取,即为二进制整数。
    // 示例1:将十进制整数 `13` 转换为二进制整数
    1.  `13 ÷ 2 = 6`,余数 `1`2.  `6 ÷ 2 = 3`,余数 `0`3.  `3 ÷ 2 = 1`,余数 `1`4.  `1 ÷ 2 = 0`,余数 `1`。
    // 从下到上读取余数,得到 `13` 的二进制表示为 `1101`
  • 十进制小数转二进制小数

    1. 乘以2取整:将十进制小数乘以2,记录整数部分。
    2. 重复操作:将小数部分继续乘以2,记录整数部分,直到小数部分为0或达到所需的精度。
    3. 读取整数部分:将记录的整数部分从上到下读取,即为二进制小数。
    // 示例2:将十进制小数 `0.625` 转换为二进制小数
    1.  `0.625 × 2 = 1.25`,整数部分 `1`,小数部分 `0.25`2.  `0.25 × 2 = 0.5`,整数部分 `0`,小数部分 `0.5`3.  `0.5 × 2 = 1.0`,整数部分 `1`,小数部分 `0`。
    // 从上到下读取整数部分,得到 `0.625` 的二进制表示为 `0.101`
  • 十进制数转二进制数

    将十进制数的整数部分和小数部分分别转换为二进制形式,然后组合在一起。

    // 示例3:将十进制数 `13.625` 转换为二进制数
    +   整数部分 `13` 转换为二进制为 `1101`+   小数部分 `0.625` 转换为二进制为 `0.101`。  
    组合整数部分和小数部分,得到 `13.625` 的二进制表示为 `1101.101`

十进制数转换为二进制数的过程涉及将整数部分和小数部分分别转换为二进制形式,然后组合在一起。需要注意的是:

  1. 整数部分:通过不断除以2并取余数来转换。
  2. 小数部分:通过不断乘以2并取整数部分来转换。
  3. 精度问题:某些十进制小数可能无法精确地转换为二进制小数,导致精度误差。