在学习JavaScript过程中,对于number这个类型经常会遇到疑问,比如经典的 0.1+0.2!==0.3问题,看了很多解析还是一头雾水,干脆总结一篇博客重新整理一下进制和科学计数法的相关知识,总算搞清楚了一点。但是仍需要经常回顾,不然一段时间又会忘记T^T
JS 里的Number类型值,没有 Int 整数,全都是浮点数,并且以 IEEE 二进制浮点算术标准中所指定的双精度 64 位格式来进行表示
科学计数法
科学计数法把一个数字表示成如下形式,其中a是尾数或者叫有效数字,n是整数,其可以是负的
而二进制科学计数法是常见于计算机的浮点数表示,也就是10的乘法换成了2的乘法形式。值得注意的是这个乘法算式的左边有效数字是二进制,而右边是十进制的乘方,在计算结果时要将有效数字部分二进制转成十进制,两者才能相乘
171 = 10101011 = 1.0101011 * 2^7
1.0101011 * 2^7转回来:
1*2^0 + 0*2^(-1)+1*2^(-2) ... + 1*2^(-7) = 1.3359375
1.3359375 * 2^7 = 171
// 或者可以这么看,2的指数其实表示浮点数的移位,
// 这里也就是1.0101011的小数点向右移位7个,那么得到的数再转十进制就是原来的整数 171了
计算机中的浮点数表示
IEEE 754 二进制浮点数算数标准是目前使用最广泛的浮点数运算标准,它指定的是现实世界中的数字如何在计算机的二进制世界中进行表示的方法,大多数编程语言都使用 IEEE 754 标准来设定语言中的数字类型。
在 IEEE 754 算术标准中,一个浮点数在转成二进制科学计数法以后,可以使用三部分的二进制组合表示,一个二进制符号位,一部分二进制表示指数部分,一部分表示有效数字部分
64 位双精度使用 64 位二进制表示浮点数,用 11 位存储二进制科学计数法的指数部分,用 52 位存储有效数字部分
下面是一个十进制数使用二进制科学计数法的表示
171 = 10101011 = (-1)^0 * 1.0101011 * 2^7;
在上面的公式中,注意S和E都是十进制数,而M是二进制数,S和E需要转为二进制形式来表示,但IEEE754规范不是直接将这三部分存成二进制,而是遵循下面规则:
- S,sign,符号位,需要一位二进制来表示正负,0表示正,1表示负;
- E,exponent,指数部分,在 64 位双精度浮点数中用 11 位二进制来换算,但是都知道指数值有正负之分,所以指数值不能直接转成 11 位二进制来表示;所以 IEEE 规定了一个中间数来区分指数的正负,大于中间数的指数值就是正,小于中间数的指数值就是负。实际的指数值 E 加上这个中间数然后再转成 11 位二进制形式就得到了 64 位二进制中表示指数的部分。由下面公式得到中间数是
1023
- M,mantissa,尾数部分,上文提到科学计数法的前面小数部分就是尾数,形如:1.0101011 * 2^7尾数是1.0101011,那么在二进制科学计数法的形式里面,尾数的小数点左侧部分肯定始终保证是 1,所以在保存的时候就可以被舍弃了,只保存尾数的小数点右侧的部分,所以在 64 位双精度浮点数中,尾数有 52 位,但是实际表示的是 53 位有效数字1.xxxxx,1 后面跟 52 位部分
下面是一个十进制小数存储为二进制,再转回十进制的过程
// ...转二进制
4.5 // 整数100,小数0.5*2=1.0 ... 1
// 二进制
100.1
// 二进制科学计数法
(-1)^0 * 1.001 * 2^2
// 指数部分还需要加上1023,得到E是1025,再将1025转成11位二进制
1025 => 100 0000 0001
// 再看有效数字部分,1.001舍去小数点前面1,得到001,不足52位,后面全补0
001 0000000000000000000000000000000000000000000000000
// 最后和11位指数部分,1位符号位,拼接在一起得到完整的存储64位二进制形式
0010000000000000000000000000000000000000000000000000 10000000001 0
// DONE!
// ...转回十进制
// 尾数部分补上舍去的 1.0
1. 0010000000000000000000000000000000000000000000000000
// 指数部分转为十进制,减去1023得到指数实际值 E = 2
10000000001 = 1025
1025 - 1023 = 2
// 最后一位符号位是 S = 0,表示正,得到二进制科学计数法形式
(-1)^0 * 2^2 * 1. 0010000000000000000000000000000000000000000000000000|(二进制)
// 继续将尾数部分1. 001转十进制
1.001 = 1*2^0 + 0*2^(-1) + 0*2^(-2) + 1*2^(-3) = 1 + 1/8
// 最后三部分相乘
(-1)^0 * 2^2 * (1 + 1/8) = 4.5
// DONE!
0.1+0.2!==0.3
0.1 + 0.2 !== 0.3 的原因主要由于 0.1 和 0.2 转为二进制的时候为无限循环小数,而计算机的存储位置有限因此会做一定的截取舍入处理,再进行加减就有一定的误差了。