《众所周知》 在JavaScript中:0.1 + 0.2 !== 0.3
。
对于为什么0.1+0.2不等于0.3这个问题,相信很多同学已经能够轻松应付面试官的提问了,但是本文旨在讲清楚这背后的道理,看看究竟谁来背这个锅?
二进制
这个世界上只有10种人,一种是懂二进制的,另一种是不懂二进制的。
计算机是通过二进制的方式存储数据的,也就是对于计算机来说只有0和1。那么我们平时使用的十进制是如何与二进制互转的呢?
在十进制中,我们不难发现,任何一个数字都可以表示为:
模拟十进制表示的方法,我们对于每一个数同样可以写成一串二的幂相乘的形式:
二进制转十进制
二进制转十进制的方法是:从后向前,每位上依次乘以2的n(n = 0,1,2,3...)次方的累加。 以二进制数字01101011
为例:
1 * 2^0 = 1
1 * 2^1 = 2
0 * 2^2 = 0
1 * 2^3 = 8
0 * 2^4 = 0
1 * 2^5 = 32
1 * 2^6 = 64
0 * 2^7 = 0
上述结果累加,结果为107(1 + 2 + 0 + 8 + 32 + 64 + 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
;
对于整数部分,除以2得0即结束,但是对于小数部分,乘以2得1时才结束,那么对于0.1、0.3
等数字会无限的循环下去,怎么办呢? 如求0.1
的二进制:
0.1 * 2 = 0.2
0.2 * 2 = 0.4
0.4 * 2 = 0.8
0.8 * 2 = 1.6
// 1.6超过1,取小数部分0.6继续乘2
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.1的二进制是0.0001100110011001100...
(0011循环),那JavaScript是如何处理无限循环的二进制小数呢?
JS中的数字存储
一般我们认为数字包括整数和小数,但是在 JavaScript 中只有一种数字类型:Number
,遵循IEEE 754
标准,使用64位
固定长度来表示,也就是标准的double双精度浮点数
。双精度浮点数存储时使用科学记数法。其存储模型位:
- 蓝色部分:用来存储符号位(sign,简写S),用来区分正负数(0-正,1-负);
- 绿色部分:用来存储指数(exponent,简写E),占用11位;
- 红色部分:用来存储小数(fraction,简写F),占用52位,多出的用0补齐;
对应公式表示为:
Q:为什么2的指数是E-1023?
A:exponent占11位,可表示0~2047的数字,对半分开,0~1022减1023为负指数,1023~2047减1023为正指数,使得指数取值范围是-1023~1024
Q:为什么M要加1?
A:二进制的数字转换成科学记数法后,都是类似于1.xxxx * 2^n
的形式,小数点前肯定是1,所以fraction只存储小数点后面的小数部分。
所以对于上一节的数字521.125
,二进制为1000001001.001
,转换成科学记数法为1.000001001001 * 2^9
,即S为0,E为1032(1032转为二进制为10000001000),M为000001001001,所以它的存储就是:
可以使用在线工具验证:Double (IEEE754 Double precision)
0.1 + 0.2的计算
以0.1
为例:
- 计算出其二进制为:
0.00011001100110011001100110011001100110011001100110011001100110011...
- 转换成科学记数法表示:
1.1001100110011001100110011001100110011001100110011001*2^-4
- 存储(为了更直观,我以*分割开S、E、F区,实际没有):
0*01111111011*1001100110011001100110011001100110011001100110011010
同理,0.2
的存储结果为:
0*01111111100*1001100110011001100110011001100110011001100110011010
计算0.1 + 0.2
,先都转化为二进制,分别是:
0.00011001100110011001100110011001100110011001100110011010
0.00110011001100110011001100110011001100110011001100110100
然后相加得:
0.01001100110011001100110011001100110011001100110011001110
上述结果转换成十进制是:0.30000000000000004
。
所以说,这不是JavaScript的精度问题,而是IEEE-754标准中的double双精度浮点数的精度问题。
最大安全数
最大安全数指的是能够one-by-one表示的整数。
先说结论:JS中的最大安全数是 2^53-1
(9007199254740991)。
第二节说到因为尾数M是52位的,加上二进制科学记数法的首位1,所以最大可以表示 1.11111(小数点后52个1)* 2 ^52
。在这个范围内的整数和二进制存储都是一一对应的,所以称2^53 -1
为最大安全数。
// 求2^53-1的二进制
console.log((2^53-1).toString(2));
// 输出:11111111111111111111111111111111111111111111111111111
// 科学计数法表示:
// 1.1111111111111111111111111111111111111111111111111111 * 2^52
为什么2^53就不行了?
2^53
的二进制为:
100000000000000000000000000000000000000000000000000000 // 注意是54位
由于存储时尾数M的长度限制,会省略掉后边的一个0,这样就造成了同一个二进制存储对应两个十进制数字。
所以会出现下面的情况:
(2^53, 2^54)
之间的数会两个选一个,只能精确表示偶数(2^54, 2^55)
之间的数会四个选一个,只能精确表示4个倍数
JS中最大的数是多少?
根据第二节可知最大的正指数是1024,所以JS中最大的可表示的数字是 2^1024 -1 ,在JS中 2^1024 就是Infinity
了,最大的数字被认为是无穷大的数字 。
2 ** 1024 === Infinity // true
怎么应对这些问题?
-
小数计算精度问题:
- 第三方库:
number-precision.js
或Math.js
; - ES6提供的
Number.EPSILON
;
function numberepsilon(arg1, arg2){ return Math.abs(arg1 - arg2) < Number.EPSILON; } console.log(numberepsilon(0.1 + 0.2, 0.3)); // true
- 第三方库:
-
大数问题
- 第三方库:
bigNumber.js
; - ES6提供的
bigint
;
- 第三方库:
总结
JavaScript不背这个锅!