js浮点数计算

语言基础

数据类型值在计算机底层都是以二进制来进行存储的,与大部分现代编程语言(包括几乎所有的脚本语言)一样,JavaScript 中的数字类型是基于 IEEE 754 标准来实现的,该标准通常也被称为“浮点数”。JavaScript 使用的是“双精度”格式(即 64 位二进制)。

64位又可分为三个部分:

  • 符号位(S):第 1 位是正负数符号位(sign),0代表正数,1代表负数
  • 指数位(E):中间的 11 位存储指数(exponent),用来表示次方数
  • 尾数位(M):最后的 52 位是尾数(mantissa),超出的部分自动进一舍零

bit index

进制转换基础

二进制转换

浮点数转换成二进制,我们要将整数部分和小数部分分开:

  • 整数部分 除二取余,倒序排列
  • 小数部分 乘二取整,顺序排列

例如,11.125 转换为二进制:

1
2
3
4
5
6
7
8
9
10
11
12
// 整数部分
第一步:11 除以 2,得到 5,余数为 1
第二步:5 除以 2,得到 2,余数为 1
第三步:2 除以 2,得到 1,余数为 0
第四步:1 除以 2,得到 0,余数为 1
所以整数部分二进制为 1011

// 小数部分
第一步:0.125 乘以 2,得到 0.25,整数部分为 0,小数部分为 0.25
第二步:0.25 乘以 2,得到 0.50,整数部分为 0,小数部分为 0.50
第三步:0.50 乘以 2,得到 1.00,整数部分为 1,小数部分为 0.00
所以小数部分二进制为 001

所以 11.125 转换为二进制为 1011.001

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* 整数部分转换为二进制
* @param {*} val 整数部分
* @returns 二进制字符串
*/
function integer2binary(val) {
// 整数部分转换为二进制结果字符串
let result = "";
// 整数部分转换为二进制
while (val !== 0) {
result = (val % 2 === 1 ? "1" : "0") + result;
val = Math.floor(val / 2);
}
return result || "0";
}

/**
* 浮点数转换为二进制
* @param {*} decimal 浮点数
* @param {*} bitLength 二进制位数
* @returns 二进制字符串
*/
function decimal2binary(decimal, bitLength = 52) {
// 二进制结果字符串
let result = "";
// 当前循环次数
let currentLoopCount = 0;
// 小数部分转换为二进制
while (decimal !== 0 && currentLoopCount < bitLength + 53) {
result += decimal * 2 >= 1 ? "1" : "0";
// 乘积的余数,继续下一次循环转换
// 使用字符串截取余数,尽量避免精度问题
decimal = Number(String(decimal * 2).substring(1));
currentLoopCount++;
}
return result;
}

/**
* 浮点数转换为二进制
* @param {*} val 浮点数
* @param {*} bitLength 二进制位数
* @returns 二进制字符串
*/
function binaryConversion(val, bitLength = 52) {
// 二进制结果字符串
let result = "";
// 整数部分
let integer = Math.floor(val);
result += integer2binary(integer);
// 小数部分
let remainder = val % 1;
if (remainder !== 0) {
result += ".";
result += decimal2binary(remainder, bitLength);
}
return result;
}

十进制转换

二进制转换为十进制时:

  • 整数部分由右往左分别对应2的递增幂,最右边的数位对应20 = 1,右数第二位对应21,右数第三位对应22,……以此类推,若二进制数由n个数位构成,则最左边的数位对应2{n-1},而后将每个数位上的数字与该数位对应的2的幂相乘,求和。

  • 小数转换将整数部分从左往右逐位进行的降幂继续延伸至小数点以后,即小数点后第一位对应2{-1},小数点后第二位对应2{-2},……以此类推,若二进制数由n个数位构成,则最右边的数位对应2{-n},而后将每个数位上的数字与该数位对应的1/2的幂相乘,求和。

  • 整数部分: 1*(23) + 0*(22) + 1*(21) + 1*(20) = 11

  • 小数部分: 0*(2-1) + 0*(2-2) + 1*(2-3) = 0.125

所以 1011.001 转换为十进制为 11.125

浮点计算

浮点值的精确度最高可达 17 位小数,但在算术计算中远不如整数精确。

1
2
3
if (a + b == 0.3) { // 别这么干!
console.log("You got 0.3.");
}

这里检测两个数值之和是否等于 0.3。如果两个数值分别是 0.05 和 0.25,或者 0.15 和 0.15,那没问题。但如果是 0.1 和 0.2 得到的不是 0.3,而是 0.300 000 000 000 000 04。测试将失败。因此永远不要测试某个特定的浮点值。

之所以存在这种舍入错误,是因为使用了 IEEE 754 数值,这种错误并非 ECMAScript 所独有。其他使用相同格式的语言也有这个问题。

进制转换

我们先把 0.1 和 0.2 和 0.3 转换成二进制

0.1 转换为二进制

1
2
3
4
5
6
7
8
9
// 0.1转换为二进制

第一步:0.1 乘以 2,得到 0.2,整数部分为 0,小数部分为 0.2。
第二步:0.2 乘以 2,得到 0.4,整数部分为 0,小数部分为 0.4。
第三步:0.4 乘以 2,得到 0.8,整数部分为 0,小数部分为 0.8。
第四步:0.8 乘以 2,得到 1.6,整数部分为 1,小数部分为 0.6。
第五步:0.6 乘以 2,得到 1.2,整数部分为 1,小数部分为 0.2。
第六步:0.2 乘以 2,得到 0.4,整数部分为 0,小数部分为 0.4。
...

可以看出,从第六步开始,小数部分开始重复,说明 0.1 不能精确表示为二进制小数。

0.1 转换为二进制为 0.0001 1001 1001...(无限循环)

用 IEEE 754 双精度表示为:

  • 符号位(S): 0
  • 指数位(E): -4
  • 尾数位(M): 1001100110011001100110011001100110011001100110011010 (由于 M 的第53位为 1,所以尾数位(M)超出的部分自动进1,由1001进为1010)

0.2 转换为二进制

1
2
3
4
5
6
7
8
9
10
// 0.2转换为二进制

第一步:0.2 乘以 2,得到 0.4,整数部分为 0,小数部分为 0.4。
第二步:0.4 乘以 2,得到 0.8,整数部分为 0,小数部分为 0.8。
第三步:0.8 乘以 2,得到 1.6,整数部分为 1,小数部分为 0.6。
第四步:0.6 乘以 2,得到 1.2,整数部分为 1,小数部分为 0.2。
第五步:0.2 乘以 2,得到 0.4,整数部分为 0,小数部分为 0.4。
第六步:0.4 乘以 2,得到 0.8,整数部分为 0,小数部分为 0.8。
第七步:0.8 乘以 2,得到 1.6,整数部分为 1,小数部分为 0.6。
...

可以看出,从第五步开始,小数部分开始重复,说明 0.2 也不能精确表示为二进制小数。

0.2 转换为二进制为 0.0011 0011 0011…(无限循环)

用 IEEE 754 双精度表示为:

  • 符号位(S): 0
  • 指数位(E): -3
  • 尾数位(M): 1001100110011001100110011001100110011001100110011010 (由于 M 的第53位为 1,所以尾数位(M)超出的部分自动进一, 由1001进为1010)

对阶

对阶的目的是为使两个浮点数的尾数能够进行加减运算。只有使两浮点数的指数值部分相同,才能将相同的指数值作为公因数提出来,然后进行尾数的加减运算。

对阶的具体方法是:首先求出两浮点数阶码的差,将小阶码加上差值,使之与大阶码相等,同时将小阶码对应的浮点数的尾数右移相应位数,以保证该浮点数的值不变。

0.1 的阶码 -4 , 0.2 的阶码 -3, 对阶阶段,将 0.1 的阶码变为 -3,然后 0.1 的尾数部分为:

1
2
3
4
// 0.1 对阶后
e = -3; m = 0.1100110011001100110011001100110011001100110011001101
// 0.2 对阶后
+ e = -3; m = 1.1001100110011001100110011001100110011001100110011010

尾数计算

1
2
3
  e = -3; m =  0.1100110011001100110011001100110011001100110011001101
+ e = -3; m = 1.1001100110011001100110011001100110011001100110011010
= e = -3; m = 10.0110011001100110011001100110011001100110011001100111

结果规格化

在机器中,为保证浮点数表示的唯一性,浮点数在机器中都是以规格化形式存储的。对于IEEE754标准的浮点数来说,就是尾数必须是1.M的形式。由于在进行上述两个定点小数的尾数相加减运算后,尾数有可能是非规格化形式,为此必须进行规格化操作。

1
2
3
4
// 对阶计算
e = -3; m = 10.0110011001100110011001100110011001100110011001100111
// 结果规格化
e = -2; m = 1.0011001100110011001100110011001100110011001100110100

0.1 + 0.2 结果转换为十进制为:

总结

为什么 0.1 + 0.2 结果不是 0.3?

    1. 0.1 和 0.2 在转换成为二进制小数时,由于 尾数位(M) 的第53位均为 1,所以尾数位(M)超出的部分自动进1,从而造成运算结果偏差1。
    1. 在进行对阶计算时,由于 0.1 的阶码为 -4,而 0.2 的阶码为 -3,所以 0.1 的尾数右移 1 位后,从而造成运算结果偏差1。
    1. 在结果规范化的过程中,又造成了运算结果偏差3 。

所以 0.1 + 0.2 结果偏大于 0.3,即 0.1 + 0.2 的二进制存储值偏大于 0.3。

那为什么 0.05 + 0.250.15 + 0.15 的值都和 0.3 相同呢,利用上述分析,我们可以得到:

并不是 0.05 + 0.25 和 0.15 + 0.15 的值等于 0.3,而是结果规范化后的值在精确度最高为 17 位小数的前提下,和 0.3 相同。

拓展

参考链接:


js浮点数计算
https://www.my-web.cn/js/float-computation/
发布于
2026年5月21日
许可协议