JS位运算符

起因

之前对js的一些涉及到二进制的运算符一直似懂非懂,看到了就一脸懵逼,还得去控制台算一下。然后最近看算法的时候又看到了这个运算符,这里就简单介绍一下学习这些位运算符的过程。
注意:以下运算均不涉及到小数。

过程

移位运算符

<<

“<<”运算符执行无符号左移位运算。在移位运算过程中,符号位始终保持不变。如果右侧空出位置,则自动填充为 0;超出 32 位的值,则自动丢弃。

先说这句话是什么意思。左移位是二进制的一种运算,就是在不改变二进制数值32位长度的前提下,将每位的数字都向左移动,左边移出去的直接丢弃,右边空出来的位置用0填充。无符号就是保持符号位不变,即本来是正数,移位后一样为正数。

正数的无符号左移位运算

这里以 7 << 2 为例。

首先将7转为二进制是 0000 0000 0000 0000 0000 0000 0000 0111.
然后对其向左移两位.

            0000 0000 0000 0000  0000 0000 0000 0111    --> 7

<< 2 00 0000 0000 0000 0000 0000 0000 0001 11 --> 左边超出部分移除,坐标填充0
----------------------------------------------------
= 0000 0000 0000 0000 0000 0000 0001 1100 --> 28

得到值为 0000 0000 0000 0000 0000 0000 0001 1100.
转换为十进制为 28.即 7 << 2 = 28

然后我们对以上的运算过程做一个处理,将这些二进制转换为我们熟悉的十进制。

            0000 0000 0000 0000  0000 0000 0000 0111 = 2^2 + 2^1 + 2^0 = 7
<< 2 00 0000 0000 0000 0000 0000 0000 0001 1100 = 2^4 + 2^3 + 2^2 = 28

对移位后的算式进行合并项可得到 2^4 + 2^3 + 2^2 = (2^2 + 2^1 + 2^0) * 2^2,即 2^4 + 2^3 + 2^2 = (2^2 + 2^1 + 2^0) * 2^2 = 7 * 2^2。由此我们可得出 7 << 2 = 7 * 2^2 = 28
我们通过计算几个简单的左移位运算,与标准答案进行比较,验证一下这个结论。

4 << 1 = 4 * 2^1 = 8
12 << 1 = 12 * 2^1 = 24
66 << 3 = 66 * 2^3 = 528
100 << 5 = 100 * 2^5 = 3200
9999 << 6 = 9999 * 2^6 = 639936

在控制台中以上几个算式的结果为

控制台左移位运算

答案完全一致。说明我们的结论是正确的。当然这个结论仅限于那些二进制移位不会左移移出的数字的简单运算。当我们遇到一些简单的可以口算的左移位运算时就可以使用这个结论快速得到结果,如果对于 99999 << 66 这种较复杂的运算你也用这个结论计算,也没有人会介意。

负数的无符号左移位运算

下面我们看一下负数的左移位运算。以 -66 << 2 为例。
首先,我们先复习一下负数如何转换为二进制。
负数转换为二进制的步骤有三:

  1. 确定负数对应正数的二进制
  2. 求第一步得到的二进制的反码
  3. 第二步得到的二进制加一
-66
66 -> 0000 0000 0000 0000 0000 0000 0100 0010
反码 -> 1111 1111 1111 1111 1111 1111 1011 1101
+1 -> 1111 1111 1111 1111 1111 1111 1011 1110

然后对其向左移两位.

            1111 1111 1111 1111  1111 1111 1011 1110    --> -66

<< 2 11 1111 1111 1111 1111 1111 1110 1111 10 --> 左边超出部分移除,坐标填充0
----------------------------------------------------
= 1111 1111 1111 1111 1111 1110 1111 1000 --> 28

得到值为 1111 1111 1111 1111 1111 1110 1111 1000.然后我们将其转换成十进制。

1111 1111 1111 1111  1111 1110 1111 1000
-1 -> 1111 1111 1111 1111 1111 1110 1111 0111
反码 -> 0000 0000 0000 0000 0000 0001 0000 1000
转换 -> -264 --> 别忘了带符号

转换为十进制为 -264.即 -66 << 2 = -264

拓展
  1. 拓展1

刚刚我们计算 -66 的二进制得到的是 1111 1111 1111 1111 1111 1111 1011 1110。我们在控制台验证一下我们得到的这个二进制。
-66的二进制表示
欸,这差距有点大呀。是我们算的不对吗?肯定不是。是因为js的引擎在做处理的时候是先按无符号数字进行处理,转换完了才会加上符号。所以 -66 的二进制应该是先得出66的二进制,然后加上负号,就得到了 -1000010

  1. 拓展2

我们比较一下下面几个算式。

33 << 2 = 132
-33 << 2 = -132
66 << 2 = 264
-66 << 2 = -264
999 << 2 = 3996
-999 << 2 = -3996

是的没错,进行无符号左移位运算时,当两个数的绝对值相等时,其相同位数的移位的绝对值一定相等。

>>

“>>”运算符执行有符号右移位运算。与左移运算操作相反,它把 32 位数字中的所有有效位整体右移,再使用符号位的值填充空位。移动过程中超出的值将被丢弃。

正数的有符号右移位运算

这里以 666 >> 3 为例。

首先将666转换为二进制是 0000 0000 0000 0000 0000 0010 1001 1010
然后对其向右移三位。

        0000 0000 0000 0000  0000 0010 1001 1010        --> 666

>> 3 0 0000 0000 0000 0000 0000 0101 0011 010 --> 因为是正数所以左边填充0,右边超出部分移除
------------------------------------------------
= 0000 0000 0000 0000 0000 0000 0101 0011 --> 83

得到值为 0000 0000 0000 0000 0000 0000 0101 0011.
转换为十进制为 83.即 666 >> 3 = 83

然后我们对以上的运算过程做一个处理,将这些二进制转换为我们熟悉的十进制。

        0000 0000 0000 0000  0000 0010 1001 1010        = 2^9 + 2^7 + 2^4 + 2^3 + 2^1 = 666
>> 3 0 0000 0000 0000 0000 0000 0101 0011 010 = 2^6 + 2^4 + 2^1 + 2^0 = 28

这个规律好像不太好总结?

负数的有符号右移位运算

这里以 -666 >> 3为例。

因为是有符号的运算,所以这里不再适用上一小节说的js的特殊处理。先将-666转换为二进制。

-666
666 -> 0000 0000 0000 0000 0000 0010 1001 1010
反码 -> 1111 1111 1111 1111 1111 1101 0110 0101
+1 -> 1111 1111 1111 1111 1111 1101 0110 0110

即-666的二进制形式为 1111 1111 1111 1111 1111 1101 0110 0110,然后对其进行有符号右移位运算

        1111 1111 1111 1111  1111 1101 0110 0110        --> -666

>> 3 1 1111 1111 1111 1111 1111 1010 1100 110 --> 因为是负数所以左边填充1,右边超出部分移除
------------------------------------------------
= 1111 1111 1111 1111 1111 1111 1010 1100 --> -84

移位后得到的值为 1111 1111 1111 1111 1111 1111 1010 1100,是一个负值,我们将其转成十进制。

1111 1111 1111 1111  1111 1111 1010 1100
-1 -> 1111 1111 1111 1111 1111 1111 1010 1011
反码 -> 0000 0000 0000 0000 0000 0000 0101 0100
转换 -> -84 --> 别忘了带符号

我们对此结果进行验证。

控制台有符号右移位运算验证

可见,我们的运算是完全正确的。

>>>

“>>>”运算符执行无符号右移位运算。它把无符号的 32 位整数所有效位整体右移。对于无符号数或正数右移运算,无符号右移与有符号右移运算的结果是相同的。

正数无符号右移位运算

这里我们以 666 >>> 3为例。

首先将666转换为二进制是 0000 0000 0000 0000 0000 0010 1001 1010
然后对其向右移三位。

        0000 0000 0000 0000  0000 0010 1001 1010        --> 666

>> 3 0 0000 0000 0000 0000 0000 0101 0011 010 --> 左边空出部分填充0,右边超出部分移除
------------------------------------------------
= 0000 0000 0000 0000 0000 0000 0101 0011 --> 83

得到值为 0000 0000 0000 0000 0000 0000 0101 0011.
转换为十进制为 83.即 666 >> 3 = 83

负数无符号右移位运算

这里以 -666 >> 3为例。

因为是有符号的运算,所以这里不再适用上一小节说的js的特殊处理。先将-666转换为二进制。

-666
666 -> 0000 0000 0000 0000 0000 0010 1001 1010
反码 -> 1111 1111 1111 1111 1111 1101 0110 0101
+1 -> 1111 1111 1111 1111 1111 1101 0110 0110

即-666的二进制形式为 1111 1111 1111 1111 1111 1101 0110 0110,然后对其进行有符号右移位运算

        1111 1111 1111 1111  1111 1101 0110 0110        --> -666

>> 3 1 1111 1111 1111 1111 1111 1010 1100 110 --> 左边空出部分填充0,右边超出部分移除
------------------------------------------------
= 0001 1111 1111 1111 1111 1111 1010 1100 --> 536870828

移位后得到的值为 0001 1111 1111 1111 1111 1111 1010 1100,转成十进制为536870828。
是不是超级大。因为是无符号右移位运算,所以在左边空出部分不论正负都会填充0.

我们对此结果进行验证。

控制台无符号右移位运算验证

可见,我们的运算是完全正确的。

注意:因为对负数进行无符号右移位运算时,所得结果很大,所以在使用过程中需要格外注意。

疑问:左移位和右移位根本都是只对位置进行了移动,那么对于 x1 >> k = y1y2 << k = x2 中的 x1 等于 x2y1 等于 y2 吗?

不一定。因为我们不能确保移动过程中被丢弃的值均为0。但凡有一个1被丢弃,就不会相等。而如果被丢弃的都是0,那么 x1 === x2 y1 === y2。如下图所示。

控制台左右移位运算比较

逻辑位运算符

&

“&”运算符(位与)用于对两个二进制操作数逐位进行比较,并根据下表所示的换算表返回结果。

第一个数的位值 第二个数的位值 运算结果
1 1 1
1 0 0
0 1 0
0 0 0

这里以 66 & 33 为例。

首先将两个数转换为二进制是 0000 0000 0000 0000 0000 0000 0100 00100000 0000 0000 0000 0000 0000 0010 0001
然后对其进行与运算。

    0000 0000 0000 0000  0000 0000 0100 0010
& 0000 0000 0000 0000 0000 0000 0010 0001
-------------------------------------------- --> 按上述表格进行运算
0000 0000 0000 0000 0000 0000 0000 0000 --> 0

得出结果为 0.

负数的与运算与正数并无区别,不做讨论。

|

“|”运算符(位或)用于对两个二进制操作数逐位进行比较,并根据如表格所示的换算表返回结果。

第一个数的位值 第二个数的位值 运算结果
1 1 1
1 0 1
0 1 1
0 0 0

这里以 66 | 66 为例。

首先将两个数转换为二进制是 0000 0000 0000 0000 0000 0000 0100 00100000 0000 0000 0000 0000 0000 0100 0010
然后对其进行与运算。

    0000 0000 0000 0000  0000 0000 0100 0010
| 0000 0000 0000 0000 0000 0000 0100 0010
-------------------------------------------- --> 按上述表格进行运算
0000 0000 0000 0000 0000 0000 0100 0010 --> 66

得出结果为 66.

负数的与运算与正数并无区别,不做讨论。

^

“^”运算符(位异或)用于对两个二进制操作数逐位进行比较,并根据如表格所示的换算表返回结果。

第一个数的位值 第二个数的位值 运算结果
1 1 0
1 0 1
0 1 1
0 0 0

这里以 66 ^ 66 为例。

首先将两个数转换为二进制是 0000 0000 0000 0000 0000 0000 0100 00100000 0000 0000 0000 0000 0000 0100 0010
然后对其进行与运算。

    0000 0000 0000 0000  0000 0000 0100 0010
| 0000 0000 0000 0000 0000 0000 0100 0010
-------------------------------------------- --> 按上述表格进行运算
0000 0000 0000 0000 0000 0000 0000 0000 --> 0

得出结果为 0.

负数的与运算与正数并无区别,不做讨论。

~

“~”运算符(位非)用于对一个二进制操作数逐位进行取反操作。

这里以 ~66 为例。

首先将其转换为二进制是 0000 0000 0000 0000 0000 0000 0100 0010
然后对其进行与运算。

~   0000 0000 0000 0000  0000 0000 0100 0010
-------------------------------------------- --> 对每一位都进行取反操作
1111 1111 1111 1111 1111 1111 1011 1101 --> -67

将结果(1111 1111 1111 1111 1111 1111 1011 1101)转换为十进制

1111 1111 1111 1111  1111 1111 1011 1101
-1 -> 1111 1111 1111 1111 1111 1111 1011 1100
反码 -> 0000 0000 0000 0000 0000 0000 0100 0011
转换 -> -67 --> 别忘了符号

得出结果为 -67.

这里我们再我看几个例子。

~2 = -3
~10 = -11
~66 = -67
~99 = -100
~-99 = 98

从中我们可以看出,位非操作就是对数字加一,然后取负。我们可以写个简单的判断方法来验证。

function judgeResult(num) {
return ~num === -(num + 1)
}

judgeResult() // false
judgeResult(10) // true
judgeResult(1) // true
judgeResult(-66) // true
judgeResult(324) // true

总结

位运算符运算结果非常有趣,在平时可以多加应用,但是一定要注意可能产生大数的预算,避免产生不必要的BUG。
这篇文章只是做了一个简单的介绍。后面有空了会做一下在实际开发中的应用,虽然我可能很久都遇不到。

参考

JavaScript学习指南:JS入门教程

文章作者: JaCo Wu
文章链接: https://jacokwu.cn/blog/2020/06/09/JS位运算符/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 JaCo Wu的博客