JavaScript(十五):位操作符

181 阅读5分钟

位操作符

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

有符号整数使用 32 位的前 31 位表示整数值。第 32 位表示数值的符号,如 0 表示正,1 表示负。这一位称为符号位(sign bit)。

00000000000000000000000000000000
符号位

正值

正值以二进制格式存储。一共 31 位,每一位都是 2 的幂。第一位(第 0 位)表示 2 的 0 次方,第二位表示 2 的 1 次方,依此类推。空位以 0 填充。

比如,数值 18 的二进制格式为 00000000000000000000000000010010,精简为 10010。后者是用到的 5 个有效位:

10010
1*2**40*2**30*2**21*2**10*2**0
160020
和为 18

负值

负值以二补数(补码)的二进制编码存储。二补数通过三个步骤计算得到:

  1. 得到绝对值的二进制
  2. 把 1 都变成 0,0 都变成 1,获得一补数(反码)
  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 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"

按位与

用&表示。按位与会将两个数的每个位对齐,然后基于真值表中的规则,贵每一位执行与操作。

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

我们对数值 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

按位或

用|表示。遵循

第一个数值的位第二个数值的位结 果
111
101
011
000
其他都和按位与一样,不再重复举例。

按位异或

用^表示。遵循

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

左移

左移操作符用<<表示。会按照指定的位数将数值的所有位向左移动。

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。