为什么0.1 + 0.2!==0.3?计算机的浮点数问题
- Published on
Introduction
当我们看到下面这段代码的时候,很容易想到如果调用addScore十次,应该就会输出You win!,但是实际上并不会。
let score = 0
function addScore() {
score = score + 0.1
if (score == 1) {
console.log('You win!')
}
}
类似地,如果我们把0.1 + 0.2的结果和0.3比较,也会发现结果是false。
一个简单的回答是:计算机使用的格式(binary-floating-point)不能准确地表示 0.1 或 0.2,当代码被编译或解释时,0.1是四舍五入到该格式中最接近的数字。 事实上,这不是JavaScript的问题,而是所有使用IEEE 754浮点数标准的编程语言都会遇到的问题。
IEEE 754标准
在JavaScript中,整数和小数都只有一种类型,就是number。JavaScript使用IEEE 754标准来表示数字,这个标准定义了两种表示数字的方式:32-bit和64-bit。32-bit的叫做single precision,64-bit的叫做double precision。JavaScript使用64-bit来表示数字。
这样的存储结构优点是可以归一化处理整数和小数,结构如下:

- 蓝色部分:用来存储符号位(
sign,简写S)用来区分正负数(0-正,1-负) - 绿色部分:用来存储指数(
exponent,简写E)占用11位; - 红色部分:用来存储小数(
fraction,简写F)占用52位,多出的用0补齐
浮点数的二进制表示
以十进制数字521.125为例:
- 整数部分
521转为二进制:依次除以2取余数的逆序;
521 / 2 = 260 ...... 1
260 / 2 = 130 ...... 0
130 / 2 = 65 ...... 0
65 / 2 = 32 ...... 1
32 / 2 = 16 ...... 0
16 / 2 = 8 ...... 0
8 / 2 = 4 ...... 0
4 / 2 = 2 ...... 0
2 / 2 = 1 ...... 0
1 / 2 = 0 ...... 1 // 到0结束
对余数取逆序,所以整数部分二进制为:1000001001。
- 小数部分
0.125转为二进制:小数部分依次乘以2取结果整数的正序;
0.125 * 2 = 0.25
0.25 * 2 = 0.5
0.5 * 2 = 1 // 到1结束
对三次运算的结果取整,所以小数部分二进制为001。
合并,所以结果为1000001001.001。
浮点数的IEEE 754标准表示
接着上个例子,我们已经把十进制数字521.125转为二进制1000001001.001,接下来我们用IEEE 754标准表示这个数字。
首先将1000001001.001转为科学计数法,即1.000001001001 * 2^9,然后将9加上1023(IEEE 754规定指数E的偏移量是1023),得到1032,转为二进制为10000001000,这部分就是IEEE 754标准中的指数部分。
对于Mantisssa部分,我们只需要取小数部分的9位,然后补0到52位,所以Mantisssa部分为000001001001。
又因为521.125是正数,所以符号位为0。
所以521.125的IEEE 754表示为:
0 10000001000 000001001001
0.1 + 0.2 != 0.3
对于数字0.1来说,按下图步骤进行转换,图中1100是重复部分,它的二进制表示是0.00011001100110011001100110011001100110011001100110011010,这个二进制小数是无限循环的,所以JavaScript会四舍五入到最接近的64-bit浮点数。
0.1 * 2 = 0.2
0.2 * 2 = 0.4
0.4 * 2 = 0.8
-------------
0.8 * 2 = 1.6
0.6 * 2 = 1.2
0.2 * 2 = 0.4
0.4 * 2 = 0.8
-------------
0.8 * 2 = 1.6
0.6 * 2 = 1.2
0.2 * 2 = 0.4
0.4 * 2 = 0.8
...
用科学记数法表示为1.1001100110011001100110011001100110011001100110011010 * 2^-4,转为IEEE 754标准表示为:
0 01111011 1001100110011001100110011001100110011001100110011010
这里因为无限循环,所以只取52位,结果的精度丢失了。
同理,0.2的IEEE 754表示为:
0 01111100 1001100110011001100110011001100110011001100110011010
0.3的IEEE 754表示为:
0 01111101 0011001100110011001100110011001100110011001100110011
0.1+0.2的计算过程如下:
0 01111011 1001100110011001100110011001100110011001100110011010
0 01111100 1001100110011001100110011001100110011001100110011010
---------------------------------------------------------------
0 01111100 0011001100110011001100110011001100110011001100110010
可以看到,最后计算的结果和0.3的IEEE 754表示不一样,所以0.1 + 0.2 != 0.3。
如何解决这个问题?
对于开头我们用于引入的那个问题,
let score = 0
function addScore() {
score = score + 0.1
+ if(score >=1) {
- if(score == 1) {
console.log('You win!')
}
}
只需要把score == 1改为score >= 1,绕过score不会精确等于1的问题。
小数计算
- 使用第三方库
Math.js或number-precision.js,这两个库都提供了小数计算的方法,可以避免精度丢失的问题。 - ES6提供的
Number.EPSILON,可以用来比较两个浮点数是否相等。例如:
function isEqual(a, b) {
return Math.abs(a - b) < Number.EPSILON
}
EPSLION,是一个极小的数,用来比较两个浮点数是否相等。
大数计算
- 使用
BigInt,BigInt是ES10新增的数据类型,可以用来表示任意大的整数。例如:
let bigInt = BigInt(9007199254740991)
console.log(bigInt + BigInt(1)) // 9007199254740992n
结尾的n表示这是一个BigInt类型。
- 使用第三方库
big.js,big.js是一个用来处理大数的库,可以用来处理大数的加减乘除等运算。 例如:
let bigNum = new Big(9007199254740991)
console.log(bigNum.plus(1).toString()) // 9007199254740992
参考资料
- [1]“Decimal to IEEE 754 Floating Point Representation - YouTube,” www.youtube.com. https://www.youtube.com/watch?v=8afbTaA-gOQ
- [2]“从0.1+0.2 !== 0.3聊聊JavaScript精度问题 - 掘金,” juejin.cn. https://juejin.cn/post/7096425891816341512 (accessed Jun. 10, 2024).
- [3]Web Dev Simplified, “Why Every Computer Fails Basic Math,” YouTube, May 21, 2024. https://www.youtube.com/watch?v=qTXwRSksJPg&t=492s (accessed Jun. 10, 2024).