curTain

位运算平时用得比较少,但是在进行数字操作的时候,还是有必要了解的

数的二进制表示

接下来要介绍的操作符用于数值的底层操作,也就是操作内存中表示数据的比特(位)。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个有效位,决定了实际的值。

负数

负值以一种称为二补数的二进制编码存储。一个数值的二补数通过如下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”

位操作

位操作有 7 种操作:

  1. 按位非( ~ )
  2. 按位与( & )
  3. 按位或( | )
  4. 按位异或( ^ )
  5. 左移( << )
  6. 有符号位右移( >> )
  7. 无符号位右移( >>> )

1.按位非( ~ )

按位非~会将数值的32位二进制的每一位取反(0变为1,1变为0)。按位非的操作符的本质取操作数的负值,然后减一

真值表

a ~a
0 1
1 0

案例:

1
2
3
4
5
6
7
8
9
10
11
// ~10
0000 0000 0000 0000 0000 0000 0000 1010

// ~ 取反
1111 1111 1111 1111 1111 1111 1111 0101

// 根据上面的公式反推
// 先 - 1
1111 1111 1111 1111 1111 1111 1111 0100
// 再取反 得到 -11 8+2+1=11
0000 0000 0000 0000 0000 0000 0000 1011

2.按位与( & )

按位与&, 本质上将两个操作数的32位二进制数的每一位对齐。

真值表:

a b a&b
0 0 0
0 1 0
1 0 0
1 1 1

案例:

1
2
3
4
5
// 10&8  -> 8
0000 0000 0000 0000 0000 0000 0000 1010 // 10
0000 0000 0000 0000 0000 0000 0000 1000 // 8

0000 0000 0000 0000 0000 0000 0000 1000 // 8

3.按位或( | )

真值表:

|a|b|a | b|
|–|–|–|
|0|0|0
|0|1|1
|1|0|1
|1|1|1

案例:

1
2
3
4
5
// 10 | 8  -> 10
0000 0000 0000 0000 0000 0000 0000 1010 // 10
0000 0000 0000 0000 0000 0000 0000 1000 // 8

0000 0000 0000 0000 0000 0000 0000 1010 // 10

4.按位异或( ^ )

真值表:

a b a ^ b
0 0 0
0 1 1
1 0 1
1 1 0

案例:

1
2
3
4
5
// 10 ^ 8  -> 2
0000 0000 0000 0000 0000 0000 0000 1010 // 10
0000 0000 0000 0000 0000 0000 0000 1000 // 8

0000 0000 0000 0000 0000 0000 0000 0010 // 2

5.左移( << )

左移(<<)将32位二进制向左移动指定的位数,空缺的位将会使用0填充。左移不会影响符号位

案例:

1
2
3
4
10<<2   -> 40
0|000 0000 0000 0000 0000 0000 0000 1010 // 10
// 符号位不会被挤掉
0|000 0000 0000 0000 0000 0000 0010 1000 // 2^5+2^3 = 32+8 = 40

5.右移( >> )

右移(>>)将32位二进制向右移动指定的位数,但是保留符号位,右移空缺的符号位使用0填充

案例:

1
2
3
4
10>>2   -> 2
0|000 0000 0000 0000 0000 0000 0000 1010 // 10
// 符号位不会被挤掉
0|000 0000 0000 0000 0000 0000 0000 0010 // 2

5.无符号位右移( >>> )

无符号位右移,会将所有32位数都向右移动。对于正数来说右移和无符号位右移的结果是一致的。

案例:

1
2
3
4
5
6
7
8
9
// 10>>>2   -> 2
0000 0000 0000 0000 0000 0000 0000 1010 // 10
// 符号位不会被挤掉
0000 0000 0000 0000 0000 0000 0000 0010 // 2

// -11
1111 1111 1111 1111 1111 1111 1111 0101
// -11>>>29 -> 7
0000 0000 0000 0000 0000 0000 0000 0111 // 1+2+4=7

位操作符的用法

  • ~~向下取整

按位非,将操作数取负值再减1,对于浮点数,位操作会舍弃小数部分,再进行取反,就能获得舍去小数部分的数了

1
2
3
4
~12.3
// -13
~~12.3
// 12
  • ~判断索引是否存在

由上述表达式可知,负数取反的结果是将负数变为正数再减 1,则 ~-1 = 0

在平时使用 indexOf() 函数时,当没有找到结果时会返回 -1,此时我们可以使用取反操作来做判断

1
2
3
4
if( [1,2,3].indexOf( 5 ) > -1 ){}

// 用取反做判断
if( ~[1,2,3].indexOf( 5 ) ){}
  • 使用 & 判断奇偶

由二进制计算方式可知( 2^n+2^n-1+…+2^1+2^0 ),只有最后一位可以为奇数,则可以使用按位与 & 来判断奇数偶数

1
2
3
4
5
// 奇数
3^1 // 1

// 偶数
4^1 // 0

总结

位操作平时虽然用得少,但是技多不压身


 评论