位操作符

​ 接下来要介绍的操作符用于数值的底层操作,也就是操作内存中表示数据的比特(位)。ECMAScript中的所有数值都以 IEEE 754 64 位格式存储,但位操作并不直接应用到 64 位表示,而是先把值转换为32 位整数,再进行位操作,之后再把结果转换为 64 位。对开发者而言,就好像只有 32 位整数一样,因为 64 位整数存储格式是不可见的。既然知道了这些,就只需要考虑 32 位整数即可。

​ 有符号整数使用 32 位的前 31 位表示整数值第 32 位表示数值的符号,如 0 表示正,1 表示负。这一位称为符号位(sign bit),它的值决定了数值其余部分的格式。正值以真正的二进制格式存储,即 31 位中的每一位都代表 2 的幂。第一位(称为第 0 位)表示2^0,第二位表示 2^1 ,依此类推。如果一个位是空的,则以 0 填充,相当于忽略不计。比如,数值 18 的二进制格式为 00000000000000000000000000010010,或更精简的 10010。后者是用到的 5 个有效位,决定了实际的值(如下所示)。

10010

​ 负值以一种称为二补数(或补码)的二进制编码存储。一个数值的二补数通过如下 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 中的所有整数都表示为有符号数。不过,确实存在无符号整数。对无符号整数来说,第 32 位不表示符号,因为只有正值。无符号整数比有符号整数的范围更大,因为符号位被用来表示数值了。

​ 在对 ECMAScript 中的数值应用位操作符时,后台会发生转换: 64位数值会转换为 32 位数值,然 后执行位操作,最后再把结果从 32 位转换为 64 位存储起来。整个过程就像处理 32 位数值一样,这让二进制操作变得与其他语言中类似。但这个转换也导致了一个奇特的副作用,即特殊值 NaNInfinity位操作中都会被当成 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"

​ 实际上,尽管两者返回的结果一样,但位操作的速度快得多。这是因为位操作是在数值的底层表示 上完成的。

按位与

​ 按位与操作符用和号(&)表示,有两个操作数。本质上,按位与就是将两个数的每一个位对齐, 然后基于真值表中的规则,对每一位执行相应的与操作。

第一个数值的位第二个数值的位结 果
111
100
010
000

​ 按位与操作在两个位都是 1 时返回 1,在任何一位是 0 时返回 0。 下面看一个例子,我们对数值 25 和 3 求与操作,如下所示:

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。

按位或

​ 按位或操作符用管道符(|)表示,同样有两个操作数。按位或遵循如下真值表:

第一个数值的位第二个数值的位结 果
111
101
011
000

​ 按位或操作在至少一位是 1 时返回 1,两位都是 0 时返回 0。

​ 仍然用按位与的示例,如果对 25 和 3 执行按位或,代码如下所示:

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。

按位异或

​ 按位异或用脱字符(^)表示,同样有两个操作数。下面是按位异或的真值表:

第一个数值的位第二个数值的位结 果
110
101
011
000

​ 按位异或与按位或的区别是,它只在一位上是 1 的时候返回 1(两位都是 1 或 0,则返回 0)。

​ 对数值 25 和 3 执行按位异或操作:

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。

左移

​ 左移操作符用两个小于号(<<)表示,会按照指定的位数将数值的所有位向左移动。比如,如果数值 2(二进制 10)向左移 5 位,就会得到 64(二进制 1000000),如下所示:

let oldValue = 2; // 等于二进制10
let newValue = oldValue << 5; // 等于二进制1000000,即十进制64

​ 注意在移位后,数值右端会空出 5 位。左移会以 0 填充这些空位,让结果是完整的 32 位数值(见图 3 - 2 )。

​ 注意,左移会保留它所操作数值的符号。比如,如果-2 左移 5 位,将得到-64,而不是正 64。

有符号右移

​ 有符号右移由两个大于号(>>)表示,会将数值的所有 32 位都向右移,同时保留符号(正或负)。 有符号右移实际上是左移的逆运算。比如,如果将 64 右移 5 位,那就是 2:

let oldValue = 64; // 等于二进制1000000
let newValue = oldValue >> 5; // 等于二进制10,即十进制2

​ 同样,移位后就会出现空位。不过,右移后空位会出现在左侧,且在符号位之后(见图 3-3)。 ECMAScript 会用符号位的值来填充这些空位,以得到完整的数值。

无符号右移

​ 无符号右移用 3 个大于号表示(>>>),会将数值的所有 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。