位操作符
位操作符用于数值的底层操作,也就是操作内存中表示数据的比特(位)。ECMAScript 中的所有数值都以 IEEE 754 64 位格式存储,但位操作并不直接应用到 64 位表示,而是先把值转换为 32 位整数,再进行位操作,之后再把结果转换为 64 位。对开发者而言,就好像只有 32 位整数一样,因为 64 位整数存储格式是不可见的。所以只需要考虑 32 位整数即可。
有符号整数使用 32 位的前 31 位表示整数值。第 32 位表示数值的符号,如 0 表示正,1 表示负。这一位称为符号位(sign bit)。
0 | 0000000000000000000000000000000 |
---|---|
符号位 |
正值
正值以二进制格式存储。一共 31 位,每一位都是 2 的幂。第一位(第 0 位)表示 2 的 0 次方,第二位表示 2 的 1 次方,依此类推。空位以 0 填充。
比如,数值 18 的二进制格式为 00000000000000000000000000010010,精简为 10010。后者是用到的 5 个有效位:
1 | 0 | 0 | 1 | 0 |
---|---|---|---|---|
1*2**4 | 0*2**3 | 0*2**2 | 1*2**1 | 0*2**0 |
16 | 0 | 0 | 2 | 0 |
和为 18 |
负值
负值以二补数(补码)的二进制编码存储。二补数通过三个步骤计算得到:
- 得到绝对值的二进制
- 把 1 都变成 0,0 都变成 1,获得一补数(反码)
- 给结果加 1
基于上述步骤确定-18 的二进制表示:
18 的绝对值
0000 0000 0000 0000 0000 0000 0001 0010
计算一补数,即反转每一位的二进制值:
1111 1111 1111 1111 1111 1111 1110 1101
最后,给一补数加 1:
1111 1111 1111 1111 1111 1111 1110 1110
在把负值输出为一个二进制字符串时,我们会得到一个前面加了减号的绝对值:
let num = -18;
console.log(num.toString(2)); // "-10010"
复制代码
默认情况下,ECMAScript 中的所有整数都表示为有符号数。不过,确实存在无符
号整数。对无符号整数来说,第 32 位(第 31 位)不表示符号,因为只有正值。无符号整数比有符号
整数的范围更大,因为符号位被用来表示数值了。
在对 ECMAScript 中的数值应用位操作符时,后台会发生转换:64 位数值会转换为 32 位数值,然后执行位操作,最后再把结果从 32 位转换为 64 位存储起来。这个转换导致了一个奇特的副作用,特殊值 NaN 和 Infinity 在位操作中会被当成 0 处理。
如果将位操作符应用到非数值,会先用 Number()转换。最终结果是数值。
按位非
用~表示,返回数值的一补数(反码)。
按位非可以返回数值的负值并减 1:
let num1 = 25; // 二进制00000000000000000000000000011001
let num2 = ~num1; // 二进制11111111111111111111111111100110
console.log(num2); // -26
复制代码
并且这个操作比直接下面这样的速度要快得多:
let num1 = 25;
let num2 = -num1 - 1;
console.log(num2); // "-26"
复制代码
按位与
用&表示。按位与会将两个数的每个位对齐,然后基于真值表中的规则,贵每一位执行与操作。
第一个数值的位 | 第二个数值的位 | 结 果 |
---|---|---|
1 | 1 | 1 |
1 | 0 | 0 |
0 | 1 | 0 |
0 | 0 | 0 |
我们对数值 25 和 3 进行按位与:
let result = 25 & 3;
console.log(result); // 1
复制代码
这个过程发生了什么呢?
25 = 0000 0000 0000 0000 0000 0000 0001 1001
3 = 0000 0000 0000 0000 0000 0000 0000 0011
AND = 0000 0000 0000 0000 0000 0000 0000 0001
按位或
用|表示。遵循
第一个数值的位 | 第二个数值的位 | 结 果 |
---|---|---|
1 | 1 | 1 |
1 | 0 | 1 |
0 | 1 | 1 |
0 | 0 | 0 |
其他都和按位与一样,不再重复举例。 |
按位异或
用^表示。遵循
第一个数值的位 | 第二个数值的位 | 结 果 |
---|---|---|
1 | 1 | 0 |
1 | 0 | 1 |
0 | 1 | 1 |
0 | 0 | 0 |
同上 |
左移
左移操作符用<<表示。会按照指定的位数将数值的所有位向左移动。
let oldValue = 2;
let newValue = oldValue << 5; // 64
复制代码
数值 2 的二进制是:
10
左移 5 位后得到了:
1000000
这个二进制数的十进制是 64。
左移会保留操作数的符号。如果刚才操作的是-2,得到的就是-64。
有符号右移
有符号右移用>>表示。它会将所有的 32 位都往右移,同时保留符号。
let oldValue = 64; // 等于二进制1000000
let newValue = oldValue >> 5; // 5
复制代码
无符号右移
无符号右移用>>>表示。对于正数,操作和有符号右移一样。对于负数则不同。
let oldValue = -64; // 等于二进制11111111111111111111111111000000
let newValue = oldValue >>> 5; // 等于十进制134217726
复制代码
这是因为 -64 的二进制表示是 11111111111111111111111111000000,右移 5 位得到 00000111111111111111111111111110,转换为十进制的 134 217 726。