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入门教程

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352