3.5.2 位操作符
操作符用于数值的底层操作,也就是操作内存中表示数据的比特(位)。ECMAScript中的所有数值都以 IEEE 754 64 位格式存储,但位操作并不直接应用到 64 位表示,而是先把值转换为32 位整数,再进行位操作,之后再把结果转换为 64 位。对开发者而言,就好像只有 32 位整数一样,因为 64 位整数存储格式是不可见的。
有符号整数使用 32 位的前 31 位表示整数值。第 32 位表示数值的符号,如 0 表示正,1 表示负。这
一位称为符号位(sign bit),它的值决定了数值其余部分的格式。
负值以一种称为二补数(或补码)的二进制编码存储。
一个数值的二补数通过如下 3 个步骤计算得到:
(1) 确定绝对值的二进制表示(如,对于 -18,先确定 18 的二进制表示);
(2) 找到数值的一补数(或反码),换句话说,就是每个 0 都变成 1,每个 1 都变成 0;
(3) 给结果加 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 1101
1
----------------------------------------------
1111 1111 1111 1111 1111 1111 1110 1110
那么,-18 的二进制表示就是 11111111111111111111111111101110。要注意的是,在处理有符号整数时,我们无法访问第 31 位。
ECMAScript 会帮我们记录这些信息。在把负值输出为一个二进制字符串时,我们会得到一个前面
加了减号的绝对值,如下所示:
let num = -18;
console.log(num.toString(2)); // "-10010"
在将 -18 转换为二进制字符串时,结果得到 -10010。转换过程会求得二补数,然后再以更符合逻辑
的形式表示出来。
在对 ECMAScript 中的数值应用位操作符时,后台会发生转换:64 位数值会转换为 32 位数值,然后执行位操作,最后再把结果从 32 位转换为 64 位存储起来。整个过程就像处理 32 位数值一样,这让二进制操作变得与其他语言中类似。但这个转换也导致了一个奇特的副作用,即特殊值 NaN 和 Infinity 在位操作中都会被当成 0 处理。
如果将位操作符应用到非数值,那么首先会使用 Number() 函数将该值转换为数值(这个过程是自动的),然后再应用位操作。最终结果是数值。
-
按位非( ~ )
它的作用是返回数值的一补数。
按位非是 ECMAScript 中为数不多的几个二进制数学操作符之一。
let num1 = 25; // 二进制 00000000000000000000000000011001 let num2 = ~num1; // 二进制 11111111111111111111111111100110 console.log(num2); // -26
这里,按位非操作符作用到了数值 25,得到的结果是 -26。由此可以看出,按位非的最终效果是对数值取反并减 1,就像执行如下操作的结果一样:
let num1 = 25; let num2 = -num1 - 1; console.log(num2); // "-26"
实际上,尽管两者返回的结果一样,但位操作的速度快得多。这是因为位操作是在数值的底层表示上完成的。
-
按位与( & )
有两个操作数。
本质上,按位与就是将两个数的每一个位对齐,然后基于真值表中的规则,对每一位执行相应的与操作。
第一个数值的位 第二个数值的位 结 果 1 1 1 1 0 0 0 1 0 0 0 0
按位与操作在两个位都是 1 时返回 1,在任何一位是 0 时返回 0。
let result = 25 & 3; console.log(result); // 1
25 和 3 的按位与操作的结果是 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
如上所示,25 和 3 的二进制表示中,只有第 0 位上的两个数都是 1。于是结果数值的所有其他位都会以 0 填充,因此结果就是 1。
-
按位或( | )
有两个操作数。
第一个数值的位 第二个数值的位 结 果 1 1 1 1 0 1 0 1 1 0 0 0
按位或操作在至少一位是 1 时返回 1,两位都是 0 时返回 0。
let result = 25 | 3; console.log(result); // 27
可见 25 和 3 的按位或操作的结果是 27:
25 = 0000 0000 0000 0000 0000 0000 0001 1001 3 = 0000 0000 0000 0000 0000 0000 0000 0011 --------------------------------------------- OR = 0000 0000 0000 0000 0000 0000 0001 1011
在参与计算的两个数中,有 4 位都是 1,因此它们直接对应到结果上。二进制码 11011 等于 27。
-
按位异或( ^ )
有两个操作数。
第一个数的位 第二个数的位 结 果 1 1 0 1 0 1 0 1 1 0 0 0
按位异或与按位或的区别是,它只在一位上是 1 的时候返回 1(两位都是 1 或 0,则返回 0)。
let result = 25 ^ 3; console.log(result); // 26
25 和 3 的按位异或操作结果为 26,如下所示:
25 = 0000 0000 0000 0000 0000 0000 0001 1001 3 = 0000 0000 0000 0000 0000 0000 0000 0011 --------------------------------------------- XOR = 0000 0000 0000 0000 0000 0000 0001 1010
两个数在 4 位上都是 1,但两个数的第 0 位都是 1,因此那一位在结果中就变成了 0。其余位上的 1在另一个数上没有对应的 1,因此会直接传递到结果中。二进制码 11010 等于 26。(注意,这比对同样两个值执行按位或操作得到的结果小 1。)
-
左移( << )
会按照指定的位数将数值的所有位向左移动。
比如,如果数值 2(二进制 10)向左移 5 位,就会得到 64(二进制 1000000)
let oldValue = 2; // 等于二进制 10 let newValue = oldValue << 5; // 等于二进制 1000000,即十进制 64
注意在移位后,数值右端会空出 5 位。左移会以 0 填充这些空位,让结果是完整的 32 位数值。
注意,左移会保留它所操作数值的符号。比如,如果 -2 左移 5 位,将得到 -64,而不是正 64。
-
有符号右移( >> )
会将数值的所有 32 位都向右移,同时保留符号(正或负)。
有符号右移实际上是左移的逆运算。let oldValue = 64; // 等于二进制 1000000 let newValue = oldValue >> 5; // 等于二进制 10,即十进制 2
同样,移位后就会出现空位。不过,右移后空位会出现在左侧,且在符号位之后。
ECMAScript 会用符号位的值来填充这些空位,以得到完整的数值。 -
无符号右移( >>> )
会将数值的所有 32 位都向右移。
对于正数,无符号右移与有符号右移结果相同。64 向右移动 5 位,会变成 2;
let oldValue = 64; // 等于二进制 1000000 let newValue = oldValue >>> 5; // 等于二进制 10,即十进制 2
对于负数,有时候差异会非常大。与有符号右移不同,无符号右移会给空位补 0,而不管符号位是
什么。对正数来说,这跟有符号右移效果相同。但对负数来说,结果就差太多了。无符号右移操作符将负数的二进制表示当成正数的二进制表示来处理。因为负数是其绝对值的二补数,所以右移之后结果变得非常之大:let oldValue = -64; // 等于二进制 11111111111111111111111111000000 let newValue = oldValue >>> 5; // 等于十进制 134217726
在对 -64 无符号右移 5 位后,结果是 134 217 726。这是因为 -64 的二进制表示是 11111111111111111111111111000000
无符号右移却将它当成正值,也就是
4 294 967 232
把这个值右移 5 位后,结果是
00000111111111111111111111111110,即 134 217 726。