数字类型
# 数字类型
ECMAScript 中的 Number 类型使用 IEEE754 标准来表示整数和浮点数值。所谓 IEEE754 标准,全称 IEEE 二进制浮点数算术标准,这个标准定义了表示浮点数的格式等内容。
在 IEEE754 中,规定了四种表示浮点数值的方式:单精确度(32位)、双精确度(64位)、延伸单精确度、与延伸双精确度。像 ECMAScript 采用的就是双精确度,也就是说,会用 <code>64 位字节来储存一个浮点数</code>
# 十进制 转 二进制
在JS内部所有的计算都是以二进制方式计算的。 所以运算 0.1+0.2 时要先把 0.1 和 0.2 从十进制转成二进制。
0.1转化成二进制的算法:
- 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.1转化成二进制是:0.0001 1001 1001 1001......
0.2转化成二进制的算法:
- 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.2转化成二进制是:0.0011 0011 0011 0011......
这里要注意 0.1 和 0. 2转成的二进制是无穷的。另外在现代浏览器中是用浮点数形式的二进制来存储二进制,所以还要把上面所转化的<code>二进制转成浮点数形式的二进制</code>
# 二进制转成浮点数形式的二进制
双精度浮点数用1位表示符号位,11位表示指数位,52位表示小数位,如下图所示

- 符号位:正数为0,负数为1
- 指数位:阶数+偏移量。阶数是:2^(e-1)^-1,e为阶码的位数;偏移量是把小数点移动到整数位只有11时移动的位数。正数表示向左移,负数表示向右移
- 小数位:即二进制小数点后面的数
接下来把 0.1 转成的二进制 0.0001 1001 1001 1001...... 转成浮点数形式的二进制。
先要把小数点移动到整数位只有1,要向右移动4位,故偏移量为−4,通过指位数的计算公式2^(11-1)^-1-4 = 1019,把 1019 转成二进制为1111111011,不够11位要补零,最终得出指位数为01111111011;
小数位为100110011001......,因为小数位只能保留52位,第53位为1故进1。
转换结果如下图所示:

# 浮点数相加
先把阶码调整为相同
0.1 是 1.1001100110011…… * 2^-4,阶码是 -4,而 0.2 就是 1.10011001100110...* 2^-3,阶码是 -3,两个阶码不同,所以先调整为相同的阶码再进行计算,调整原则是小阶对大阶,也就是 0.1 的 -4 调整为 -3,对应变成 0.11001100110011…… * 2^-3
接下来是尾数计算:
0.1100110011001100110011001100110011001100110011001101
+ 1.1001100110011001100110011001100110011001100110011010
————————————————————————————————————————————————————————
10.0110011001100110011001100110011001100110011001100111
2
3
4
我们得到结果为 10.0110011001100110011001100110011001100110011001100111 * 2^-3
将这个结果处理一下,即结果规格化,变成 1.0011001100110011001100110011001100110011001100110011(1) * 2^-2
括号里的 1 意思是说计算后这个 1 超出了范围,所以要被舍弃了。
再然后是舍入,四舍五入对应到二进制中,就是 0 舍 1 入,因为我们要把括号里的 1 丢了,所以这里会进一,结果变成
1.0011001100110011001100110011001100110011001100110100 * 2^-2
本来还有一个溢出判断,因为这里不涉及,就不讲了。
所以最终的结果存成 64 位就是
0 01111111101 0011001100110011001100110011001100110011001100110100
将它转换为10进制数就得到 0.30000000000000004440892098500626
因为<code>两次存储时的精度丢失加上一次运算时的精度丢失</code>,最终导致了 0.1 + 0.2 !== 0.3
参数:https://github.com/mqyqingfeng/Blog/issues/155
# 面试
问:0.1+0.2 等于 0.3吗? 为什么不等于
答:不等于。因为 JS 是用64来保存 浮点数。1位存符号位,11位存指数位,剩下52位存小数位。而0.1 和 0.2 的二进制是 无限循环的。所以0.1、0.2转成浮点数的二进制时,会对52位后的二进制进行舍去(53位为1,进1;为0舍弃)。这是第一次精度丢失。0.1 和 0.2 浮点数二进制相加导致 溢出一个位。要进行舍弃。这是第二次精度丢失。由于以上两种<code>精度丢失</code>,所以0.1+0.2不等于0.3
问:“0.1+0.2不等于0.3会引起那些BUG?
答:
- 引起统计页面展示错乱的BUG,展示多个小数点
- 还有300.01优惠300元后,支付金额不足0.01元等类似的BUG
问:怎么解决0.1+0.2不等于0.3这个问题? 请封装一个方式来解决精度问题
答:
<code>Number.EPSILON</code>的实质是一个可以接受的最小误差范围. 如果误差能够小于Number.EPSILON,我们就可以认为结果是可靠的
function equal(left, right) {
return Math.abs(left - right) < Number.EPSILON
}
// equal(0.1-0.2, 0.1)
// false
// equal(0.1-0.2, -0.1)
// true
2
3
4
5
6
7