二进制的转换
整数转二进制
整数转二进制的规则:除2取余,逆序排列,直到商为0时停止;
// 数字45用二进制表示
45 / 2 = 22 ... 1
22 / 2 = 11 ... 0
11 / 2 = 5 ... 1
5 / 2 = 2 ... 1
2 / 2 = 1 ... 0
1 / 2 = 0 ... 1
逆序排列,所以45转二进制为:101101;
同理,二进制整数转十进制应该从右到左:
1*2^0 + 0*2^1 + 1*2^2 + 1*2^3 + 0*2^4 + 1*2^5 = 45;
小数转二进制
小数转二进制的规则:乘2取整,顺序排列,直到积为1时停止;
// 数字0.625用二进制表示
0.625 * 2 = 1.25 整数为1
0.25 * 2 = 0.5 整数为0
0.5 * 2 = 1 整数为1
顺序排列,因为是小数,所以0.625转二进制为:0.101;
二进制小数转十进制应该从左到右:
1*2^-1 + 0*2^-2 + 1*2^-3 = 0.5 + 0 + 0.125 = 0.625;
注意点
- 如果数字大于1,并且不是整数,那么它的二进制表示为整数部分和小数部分拼接,如45.625转二进制为101101.101。
- 在对有符号值进行二进制转换时,我们需要在二进制开头引入符号位,如上述45转成8位二进制应该为00101101,而-45转8位二进制应该为10101101。
- 在js中,开头为0的数值会被识别成八进制,开头为0b的数值会被识别成二进制,而十六进制的开头则为0x。
console.log(010) // 8
console.log(0b010) // 2
console.log(0x010) // 16
二进制在内存中的存储
- 原码 计算机里保存的是最原始的数字,也就是没有正和负的数字,我们称之为无符号数字,但是我们实际使用的数字,既有正数也有负数,因此就有了原码——把二进制的左边第一位空出来当成符号位,其中0表示正数1表示负数。
因此,我们如果在内存中用4位去存储二进制,那么可以表示的数值范围在-7 ~ +7。原码表如下:
| 十进制 | 二进制 | 十进制 | 二进制 |
|---|---|---|---|
| +0 | 0000 | -0 | 1000 |
| +1 | 0001 | -1 | 1001 |
| +2 | 0010 | -2 | 1010 |
| +3 | 0011 | -3 | 1011 |
| +4 | 0100 | -4 | 1100 |
| +5 | 0101 | -5 | 1101 |
| +6 | 0110 | -6 | 1110 |
| +7 | 0111 | -7 | 1111 |
注意点 用原码表示二进制其实比较好理解,但是存在两个问题:
- 存在计算问题,(+1) + (-1)的值为1010(即-2),这与我们知道结果不一样;
- +0和-0对我们来说是两个相同的值,但是存在着两种不同的二进制表达方式;
- 反码 如上面的注意点所讲述的,用原码表示二进制会存在两个问题,因此,为了解决问题1,就有了反码;
反码的定义:正数的反码是它自身,负数的反码在其原码的基础上符号位不变,其余0和1取反存储。
反码表如下:
| 十进制 | 二进制 | 十进制 | 二进制 |
|---|---|---|---|
| +0 | 0000 | -0 | 1111 |
| +1 | 0001 | -1 | 1110 |
| +2 | 0010 | -2 | 1101 |
| +3 | 0011 | -3 | 1100 |
| +4 | 0100 | -4 | 1011 |
| +5 | 0101 | -5 | 1010 |
| +6 | 0110 | -6 | 1001 |
| +7 | 0111 | -7 | 1000 |
由上面的反码表可以看出,-1的反码为1110,因此(+1) + (-1)的值为0001 + 1110 = 1111;对比反码表1111就是-0的表示。
- 补码
如果说反码只解决了原码的问题1,那么补码还同时解决了原码的问题2。 补码的定义:正数的补码是它自身,负数的补码是在反码的基础上+1。 补码表如下:
| 十进制 | 二进制 | 十进制 | 二进制 |
|---|---|---|---|
| +0 | 0000 | -0 | 0000 |
| +1 | 0001 | -1 | 1111 |
| +2 | 0010 | -2 | 1110 |
| +3 | 0011 | -3 | 1101 |
| +4 | 0100 | -4 | 1110 |
| +5 | 0101 | -5 | 1011 |
| +6 | 0110 | -6 | 1010 |
| +7 | 0111 | -7 | 1001 |
如上述补码表所示,此时的+0和-0都是0000,因此完美地解决了+0和-0的二进制表达式不相同的问题.
-0的反码+1为什么是0000,因为上述的二进制存储我们都是4位存储的,因此1111 + 1的结果为10000时,会自动舍弃除符号位的最左侧高位,此时的第四位符号位已经变成了0,而最左侧的最高位1将被舍弃,因此-0的补码就是0000。
负数取补码之后,我们能表示的最小整数也从-7(1001)变成了-8(1000),因此我们能够取值的范围也变成了-8 ~ +7。
- 移码 移码有两个主要的作用:
- 方便比较数值大小;
- 用于浮点数中"阶码"的修正(这个先放一放); 移码的定义:补码其他位不变,符号位取反。
我们知道,-2 < 2。但是在二进制中,-2为1010,2为0010,因此在进行比较时,会导致比较失败,而用补码将其符号位取反后,-2为0010,2为1010,1010 > 0010,因此就能得到正确的比较结果。
Q:求-15的补码
// 我们用32位存储去表示数值15的补码
1. 先获取绝对值(15)的二进制码为:
0000 0000 0000 0000 0000 0000 0000 1111
2. 取-15的原码,符号位为1:
1000 0000 0000 0000 0000 0000 0000 1111
3. 求反码,除符号位之外的0和1互换
1111 1111 1111 1111 1111 1111 1111 0000
4. 求补码,即反码加1
1111 1111 1111 1111 1111 1111 1111 0001
总结
二进制数在内存中最终是以补码的形式存储的;对于正数,原码,反码,补码都是相同的(即它自身的原码)
JS中的数值存储
JS的数值都是用IEEE 754标准定义的64位浮点格式存储的
什么是IEEE 754 64位格式?
如上图所示:1位符号位(S)+11位阶码(E)+52位尾数(M) 就构成了64位的双精度浮点数。
尾数
将二进制浮点数规格化之后数字的小数部分就是尾数
规格化是将二进制浮点数使用科学计数法进行表示,比如5.2的二进制为101.00110011..., 规格化之后就是1.0100110011.. * 2^2(将小数点左移或右移直到小数点左边首位为1),其中0100110011..部分就是尾数。
注意一点,这里的科学计数法除了表示0之外,其他所有数的首位都为1,因此IEEE 754标准默认省略了这个1,从而增加了尾数存储值的范围,所以有效尾数实际上是有52 + 1 = 53位的。这也是为什么JS的最大安全数是2 ^ 53 - 1的原因。
阶码
阶码是用来对浮点数加权的一个无符号整数,阶码 = 阶码真值 + 偏移量1023,其中阶码真值位规范化后科学计数法中指数的值,偏移量 = 2^(k-1)-1,k 表示阶码位数,比如8位的阶码偏移量=127,11位的阶码偏移量为1023;
为什么11位阶码的偏移量是1023?
阶码是无符号整数,所以它能存储二进制0 ~ 2047之间的值,除去0跟2047两个非规格化的值之后,它的范围为1 ~ 2046,但是规范化中的指数有可能为负数,就需要引入一个偏移量1023,将范围变成-1022 ~ 1023。不然有负数的存在,就要引入补码,就会增加计算难度。
什么是非规格化?
- 当阶码为111 1111 1111指数为1024时,就是非规格化,此时若尾数为0,那么就表示无穷大(Infinity),若尾数不为0,则表示NaN;
- 当阶码为000 0000 0000指数为-1023时,若尾数为0,则表示正负0,否则就是一个非常接近于0的数。
求-13.125在JS内存中二进制的表示?
-
因为-13.125是个负数,由此我们知道
符号位为1; -
分别求13与0.125的二进制表示:
11101和001,因此13.125的二进制表示为1101.001; -
将上述二进制按照科学技术法规格化之后,就是
1.101001 * 2 ^ 3,由此可知尾数为101001; -
此时可以知道阶码真值为4,阶码 = 阶码真值 + 偏移量1023 =
3 + 1023 = 1026,所以阶码的二进制为10000000010; -
将
符号位+阶码+尾数拼起来可得到-13.125在JS中的二进制表示为(尾数不足52位用0补全):1 100000000010 1010010000000000000000000000000000000000000000000000