资讯专栏INFORMATION COLUMN

浅谈JavaScript位操作符

fasss / 675人阅读

摘要:有符号的右移操作符由两个大于符号表示这个操作符的含义就是将数值的位向右移指定的位数同时保留符号位的值正负号标记有符号的右移操作符与左移操作符刚好相反比如向右移动位就是同样的在移位的过程中也会出

位操作符的基本概念

因为ECMAscript中所有数值都是以IEEE-75464格式存储,所以才会诞生了位操作符的概念.

位操作符作用于最基本的层次上,因为数值按位存储,所以位操作符的作用也就是操作数值的位.不过位操作符并不能操作64位的值.所以位操作符会先将64位的值转换成32位的值,然后执行操作,最后再将结果转换成64位的值.

但对于开发人员来说,这整个过程就像是只存在32位的数值一样,这是因为64位存储格式是透明的.

当然这里所说的数值指的是整数.在对于有符号的整数中,32位的前31位用于表示整数的值,而第32位则表示整数的符号(即0表示正数,1表示负数),我们把这第32个表示符号的位叫做符号位,符号位也决定了其它位的数值的格式.

正数都是按纯二进制格式存储的,在前31位中的每一个位都表示2的幂.即第一位表示2^0(2的零次方),第二位表示2^1(2的1次方),依次类推.第一位也叫做位0,后面依次类推,第32位就叫做位31,其它没有用到的位都以0填充,也可以被忽略不计.

比如十进制整数10的二进制表示是0000 0000 0000 0000 0000 0000 0000 1010或者更简单的1010。这是4个有效位,这4位就决定了实际的值.在前面说到过可以用toString()方法指定参数可以表示将一个十进制数转换成二进制数.所以我在这里写了一个函数,表示将一个十进制数转换成二进制数,如下图所示:

既然二进制数1010就是十进制数10,那么我们还可以将这个二进制数转换成十进制数,是如何计算的呢?很简单,因为二进制数最后一位表示符号,所以不计,这里的101各代表幂数为3,2,1,这也是为什么十进制转换成二进制数要取余数倒排的原因,然后将位上的数乘以基数2的幂数.也就是说可以写成等式2 ^ 3 * 1 + 2 ^ 2 * 0 + 2 ^ 1 * 1 = 10.(2 ^ *表示2*次方).

负数同样以二进制码存储,只不过与正数有点区别,区别就是负数的格式是二进制补码.在求二进制补码的时候,有以下三个规则:

(1).先求出这个负数的绝对值的二进制码.比如十进制数-17,就是先求17的二进制码.

(2).然后求二进制码的反码,就是将0变成1,1变成0.

(3)最后将得到的二进制反码加1.

比如说求十进制数-10的二进制码,我们要先求10的二进制码,也就是
0000 0000 0000 0000 0000 0000 0000 1010,然后取反码就是
1111 1111 1111 1111 1111 1111 1111 0101,最后加1,但因为二进制数只能是1或者0表示,所以1+1大于2的话,就会向前进位1.所以这个反码加1最后得到的值应该是1111 1111 1111 1111 1111 1111 1111 0110.而这个也是-10的二进制表示.需要注意的是在处理有符号的整数的时候,是访问不到第32位的(也就是位31).

但在实际情况中,ECMAscript是会尽力向我们隐藏所有的这些信息.也就是说在实际转换负数的二进制码时,它只会将这个负数的绝对值的二进制码前面加上一个负号,就表示这个负数的二进制码.如下图所示:

这个转换过程说明ECMAscript解析引擎理解了二进制补码并将其以更合乎逻辑的形式展示出来.

在默认情况下,ECMAscript中的所有整数都是有符号整数.当然也存在无符号整数,对于无符号的整数来说,第32位不会再表示符号,因为无符号整数只能是正整数.而且无符号整数的值可以更大,因为第32位不再表示符号,而可以表示成数值.什么意思呢?就是说当我们再将十进制数转换成二进制数时,必须要除到商为0时,才会倒排余数,而第32位恰好就是商为0的那个余数.而正整数值越大,我们可以省略的有效位数就越多,此时值也就越大.

ECMAscript中,当对数值应用位操作符的时候,虽然后台会发生将64位数值转换成32位数值,然后执行完操作之后,再转换成64位的数值这个转换过程.但正因为这个转换过程导致了一个严重的副效应,也就是说在对特殊的NaNInfinity值应用位操作符时,这两个值会被当成0来处理.

而如果对非数值应用位操作符,会自动使用Number()函数将其转换成一个数值来操作,然后再应用位操作符,得到的结果也将是一个数值.

总的说来,位操作符主要包含按位非(NOT),按位与(AND),按位或(OR),按位异或(XOR),左移,无符号右移和有符号右移7个操作符.接下来,咱们就来一一分析这7个操作符.

a.按位非(NOT)

按位非用一个波浪线符号"~"表示,执行按位非的结果就是取得数值的反码.它也是ECMAscript中少数几个与二进制计算相关的操作符.

比如求10的按位非结果,那么按照求二进制得到10的二进制码是0000 0000 0000 0000 0000 0000 0000 1010,然后取反码就是1111 1111 1111 1111 1111 1111 1111 0101.而要将这个反码转换成十进制数,还需要以下过程:

此时,位31上的1代表符号为负,因为负数的补码就是反码加1,所以得知负数的反码就等于补码减1,所以此时求得负数的反码是1111 1111 1111 1111 1111 1111 1111 0100,所以负数的原码就是取反,变成了0000 0000 0000 0000 0000 0000 0000 1011,所以此时再将这个二进制数转换成十进制数就是-(2 ^ 3 * 1 + 2 ^ 2 * 0 + 2 ^ 1 * 1 + 2 ^ 0 * 1)=-11.要理清这个转换过程,需要知道什么是反码,什么是原码,什么又是补码,因为参与计算的是补码,而要转换的是求原码.也就是说,要想将二进制反码转换成十进制数,就必须求得二进制反码的原码,然后对原码直接按照二进制转换成十进制的方式来计算转换.现在我们来验证一下是否是我们所想的,如下图所示:

再比如求-10的按位非结果,按照理论分析,我们从前述可以得知最终-10的二进制码为1111 1111 1111 1111 1111 1111 1111 0110,取反码就变成了0000 0000 0000 0000 0000 0000 0000 1001,而此时的二进制反码的补码,原码都一样,所以直接计算就是2 ^ 3 * 1 + 2 ^ 2 * 0 + 2 ^ 1 * 0 + 2 ^ 0 * 1 = 9.如下图所示:

通过以上示例还应该得到一个结论:正整数的二进制码的反码与原码补码不一致,而负整数的二进制码的反码就与原码补码一致.换句话说,就是正数的原码与补码一样,负数的原码与补码不一样.

如果实在是不能理解原码,补码与反码,可以直接把这个操作符理解为数值加1取反.如101取反就变成-11,-101取反就变成9.

而实际上,对按位非的结果比如~10~-10,我们还可以写成如下图所示的表示:

我们可以用变量来表示,如下图所示:

虽然不用按位非操作符的以上所表示的代码也能输出同样的结果,但由于按位非是对底层进行操作,所以使用按位非操作符的速度会更快.

b.按位与(AND)

按位与操作符用一个和号字符(&)表示,它有两个操作数,从本质上讲,按位与操作就是将数值的每一位二进制码对齐,然后根据以下规则,对相同为止上的两个数执行AND操作.规则如下:

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

简而言之,就是只在两个数值的位数都对应为1的时候,结果才为1,任何一位是0,结果都是0.

如以下示例:

106进行按位与操作时返回2,这是为什么呢?请看底层原理:

首先10转换成二进制数就是0000 0000 0000 0000 0000 0000 0000 1010,而6转换成二进制数则是0000 0000 0000 0000 0000 0000 0000 0110.过程可以如下:

10 = 0000 0000 0000 0000 0000 0000 0000 1010
 6  = 0000 0000 0000 0000 0000 0000 0000 0110
——————————————————————————————————————————————
AND = 0000 0000 0000 0000 0000 0000 0000 0010

然后按位与结果转换成十进制数就是2 ^ 1 * 1 = 2.所以最终结果为2.

再比如求2 & 5的结果,现在咱们按照步骤来计算出结果,然后再验证答案对不对.

首先求得2的二进制数为0000 0000 0000 0000 0000 0000 0000 0010,5的二进制数为0000 0000 0000 0000 0000 0000 0000 0101。 

 2 = 0000 0000 0000 0000 0000 0000 0000 0010
 5  = 0000 0000 0000 0000 0000 0000 0000 0101
——————————————————————————————————————————————
AND = 0000 0000 0000 0000 0000 0000 0000 0000

而这个结果转换成十进制数就是0。所以得出结果是0,现在咱们来验证一下,如下图所示:

c.按位或(OR)

   按位或操作符由一个竖线符号(|)表示,同样也有两个操作数.从本质上讲,也可以说是将数值的二进制码对齐,但与按位与操作符有一点点区别,就是它的规则与按位与操作符不一样,具体如下:

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

简而言之,就是按位或操作符只有其对应的两个位都是0的情况下才是0,其它有一个位是1的情况下都是1.如以下示例:

现在,我们就来分析一下为什么结果是7,其实与按位与的底层操作很相似,2和5的二进制数前述示例已求得:

2 = 0000 0000 0000 0000 0000 0000 0000 0010
5  = 0000 0000 0000 0000 0000 0000 0000 0101
——————————————————————————————————————————————
OR = 0000 0000 0000 0000 0000 0000 0000 0111

而将按位或的结果转换成十进制数就是2 ^ 2 * 1 + 2 ^ 1 * 1 + 2 ^ 0 * 1 = 7。所以结果7就是这么求来的。

d.按位异或(XOR)

 按位异或操作符由一个插入符号(^)表示,也有两个操作数,其本质也与按位与和按位或操作符相同,但其规则也不一样,如下:

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

也就是说,按位异或操作符只有在其中一个位为1时才返回1,否则就是0。如对2 ^ 5求结果如下图:

现在来分析一下为什么结果是7,过程也与求按位与和按位或结果一致.

2 = 0000 0000 0000 0000 0000 0000 0000 0010
5  = 0000 0000 0000 0000 0000 0000 0000 0101
——————————————————————————————————————————————
XOR = 0000 0000 0000 0000 0000 0000 0000 0111

这里因为对应位没有变化,所以最终结果才会和按位或结果一致。

e.左移

  左移操作符由两个小于号(<<)表示,也是两个操作数,第一个操作数就表示要左移的数值,第二个操作数表示左移的位数.所以左移操作符的含义就是将数值的所有位向左移动指定的位数.

而在向左移动了指定的左移位数之后,原数值的右侧会多出指定的位数个空位(比如指定左移4位,也就多出4个空位,依次类推)出来,不过左移操作会自动以0来填充这些空位.

如以下示例:

现在来分析一下为什么结果是40,首先5的二进制数是
0000 0000 0000 0000 0000 0000 0000 0101,指定的是向左移动3位,所以整体向左移动3位,就变成了0000 0000 0000 0000 0000 0000 0010  1000,而这个二进制转换成十进制数就是2 ^ 5 * 1 + 2 ^ 3 * 1 = 40.
所以最终结果就是40.

注意,左移操作并不会影响操作数的符号位,换句话说,如果将-5左移3位,结果将是-40,而不是40.

f.右移操作符

 右移操作符又分为无符号右移有符号右移操作符.

(1).有符号的右移操作符。

 有符号的右移操作符由两个大于符号表示(>>),这个操作符的含义就是将数值的位向右移指定的位数,同时保留符号位的值(正负号标记),有符号的右移操作符与左移操作符刚好相反,比如40向右移动3位就是5.

同样的,在移位的过程中,也会出现空位,而这时候,ECMAscript会用符号位的值来填充所有空位,也就是说每向右移动一位,移走的位上的数不管是1,还是0都会消失了,则会在数值的左侧补充一位,而这位的值就是符号位的值,即如果是正数,补充0,负数补充1.

如以下一个示例:

现在,咱们就来分析分析为什么最终结果为0.首先由前述可以得知40的二进制数为0000 0000 0000 0000 0000 0000 0010  1000,指定的是向右移动3位,那么整体向右移就变成了0000 0000 0000 0000 0000 0000 0000 0101,这个转换成十进制数也就是5.所以才会说有符号的右移与左移结果相反.

再来看一个示例:

现在,咱们就来分析分析为什么最终结果为0.首先由前述可以得知5的二进制数为0000 0000 0000 0000 0000 0000 0000 0101,指定的是向右移动3位,那么整体向右移3位,左侧就要补充符号位的值,因为是正数(正数符号表示为0),所以补充30,就变成了
0000 0000 0000 0000 0000 0000 0000 0000.所以最终结果为0

如果这样不能理解的话,那么假设向右移动一位,也就是求5 >> 1的结果,同样在最左侧补充一个符号位的值0,右移走了末位的1.所以变成了0000 0000 0000 0000 0000 0000 0000 0010.这个转换成十进制数就是2.现在咱们来操作验证一下,如下图:

(2).无符号右移操作符。

   无符号右移操作符由三个大于符号表示(>>>).这个操作符也是会将所有的32位都整体向右移动指定的位数.对于正数来说,其实无符号右移操作符和有符号右移操作符的结果一致.

5 >>> 1仍然是2,按照同样的过程步骤分析.

对于正数没有什么变化,但对于负数来说,变化可就大了,首先无符号右移操作符是以0填充空位,而不是像有符号右移操作符那样以符号位的值填充.所以才会正数与有符号右移操作符的结果相同.但是负数就不一样了,无符号右移操作符会把负数的二进制码当成正数的二进制码,而且负数是由其绝对值的二进制补码表示,因此导致无符号右移之后结果会很大.换句话说,就是对负数进行无符号右移操作时只会返回正数.

如求-5 >>> 3.我们先自己求一遍,首先-5的二进制补码为1111 1111 1111 1111 1111 1111 1111 1010,而因为无符号右移会把这个补码当成正数的二进制码,所以转换成十进制数就是(口算不太现实,太大了,还是让计算机来算吧)如下图所示:

所以就会被当成4294967290,然后这个正数的二进制码右移3位变成了0001 1111 1111 1111 1111 1111 1111 1111,转换成十进制数就是如下图所示:

所以最终结果就是536870911.现在,我们来验证一下,如下图所示:

前端面试题分析

知道了位操作符之后,现在咱们来分析一道题,有这样一道前端面试题,写一个函数用于判断一个非负整数是否是2的非负整数次幂.而有人曾经这样写,如下图所示:

那么为什么这样写呢,我们来分析一下这其中原理,首先什么是函数,使用function关键字声明的都可以被叫做函数,而这里定义的函数名也比较语义化,叫做isPowerOfTwo,圆括号中的n叫做函数的参数,顾名思义,这里的参数就是传入一个非负整数.而这个函数的作用就是要判断传入的参数(即非负整数)是否是2的非负整数次幂.

return也是一个关键字,表示返回一个值,用在函数当中,而要记住的是,如果在函数当中写入了return关键字,在这个关键字表示的语句结束后面再写其它语句是没有效果的,如下图所示:

如上图所示,alert()方法表示弹出一个原生的弹出框,但实际上在调用这个定义的判断函数之后,是不会执行弹出框的,这就是return关键字在这里起到的作用.

现在再来分析一下里面的结构,叹号(!)也就是逻辑非的意思,这个操作符会把一个操作数转换成布尔值,然后取反.

再来看圆括号里面的和字符号&,在学了位操作符之后,我们就应该知道这个符号就是按位与的意思,而按位与是操作二进制数的位的,对应规则也应该知道,就是当两个操作数(在这里指nn-1)的对应位都是1时,最终返回的对应位结果才是1,否则就是0.按位与的作用就是将位对齐.所以,在返回这个结果之前,我们还需要知道如何转换成二进制数.

我们应该知道对象的toString()方法,可以为其指定一个参数为基数2,就可以将一个操作数转换成二进制数返回,当然这里也是返回一个字符串.而为了方便,我将这个方法封装在一个函数中,如下图所示:

现在我们再来看看一个非负整数如果是2的幂,会有什么特点,我们可以调用以上的定义函数将一个非负整数转换成二进制数,而一个非负整数如果是2的幂,我们应该知道2的幂有2 ^ 0 = 1,2 ^ 1 = 2,2 ^ 2 = 4......依次类推,我们从而得知1,2,4,8,16....等就是2的幂,而我们将这些值转换成二进制数,就可以知道有什么样的关系了,比如1转换成二进制就是1,2转换成二进制是10,4转换成二进制是100......依此类推,不信咱们可以用上面定义好的函数来验证,如下图:

现在我们就应该知道规律了,如果一个非负整数是2的非负整数次幂的话,那么这个数一定是上一个2的非负整数次幂的二进制数左移了一位.而通过之前知道的左移操作符,我们知道,左移就是将位往左移动一位,然后在移动后的空位中以0填充.

现在,我们再来看看n-1,假设是2的非负整数次幂的非负整数,减1,然后再将其转换成二进制数,比如12的非负整数次幂,1 - 1 = 0.转换成二进制就是0(这里是简写),再比如2 - 1 = 1的二进制就是1,4 - 1 = 3的二进制就是11,7就是111.不信我们可以通过以上定义的函数来验证,如下图所示:

通过使用按位与操作符取得非负整数与非负整数减1的结果,不言而喻,始终都会返回0,为什么呢?因为对应位的关系,我们取其中一个为例子,如下:

0 = 0000 0000 0000 0000 0000 0000 0000 0000
1 = 0000 0000 0000 0000 0000 0000 0000 0001
——————————————————————————————————————————————
AND = 0000 0000 0000 0000 0000 0000 0000 0000

所以最终结果就是二进制数0000 0000 0000 0000 0000 0000 0000 0000,转换成十进制数就是0.

这样,我们就应该知道了,如果这个非负整数是2的非负整数次幂的话,那么它与它减1两个操作数取按位与结果就应该是0.

而我们知道逻辑非操作符对数值0会返回true的布尔值,所以当如果传入的参数是非负整数,并且还是2的非负整数次幂的话,那么这个函数最终就会返回true.我们可以直接调用这个函数,如下图所示:

理解和掌握JavaScript位操作符,有助于我们研究底层原理。

鄙人创建了一个QQ群,供大家学习交流,希望和大家合作愉快,互相帮助,交流学习,以下为群二维码:

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/101960.html

相关文章

  • 浅谈Javascript事件委托(代理)

    摘要:开玩笑啦,提供一种方法叫做事件委托。途中经过各个层次的,并在各上触发捕获事件,直到到达时间的目标。懂得了事件冒泡的过程,就很容易明白事件委托的运作原理。 首先祝大家七夕快乐。。假如现在有一个的列表,里面可能会有若干个列表项。现在要为每一个列表项绑定相同的点击事件,现在你可能会有这几种做法: 手动为每一个列表项绑定事件; 在onload的时候,找到该列表,对其每一个子元素进行遍历,循环...

    yunhao 评论0 收藏0
  • 浅谈V8引擎中的垃圾回收机制

    摘要:新生代的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。分别对新生代和老生代使用不同的垃圾回收算法来提升垃圾回收的效率。如果指向老生代我们就不必考虑它了。 这篇文章的所有内容均来自 朴灵的《深入浅出Node.js》及A tour of V8:Garbage Collection,后者还有中文翻译版V8 之旅: 垃圾回收器,我在这里只是做了个记录和结合 垃圾回收...

    happen 评论0 收藏0
  • 浅谈JS中的数据类型转换

    摘要:关于中的各种数据类型的简单转换。转换为布尔值全局方法方法注意要区分空字符串和有空格的字符串。如果预期某个位置应该是布尔值,会将该位置上现有的值自动转为布尔值。常规转换取反两次,对应的布尔值不变。 关于JavaScript中的各种数据类型的简单转换。 转换为字符串 toString 可以用toString这个API将其他数据类型转换为字符串,其中也有一些特例。 var a = 1; a....

    CarlBenjamin 评论0 收藏0
  • 浅谈Java并发编程系列(七) —— 深入解析synchronized关键字

    摘要:第一个字被称为。经量级锁的加锁过程当一个对象被锁定时,被复制到当前尝试获取锁的线程的线程栈的锁记录空间被复制的官方称为。根据锁对象目前是否处于被锁定状态,撤销偏向后恢复到未锁定或经量级锁定状态。 Synchronized关键字 synchronized的锁机制的主要优势是Java语言内置的锁机制,因此,JVM可以自由的优化而不影响已存在的代码。 任何对象都拥有对象头这一数据结构来支持锁...

    piglei 评论0 收藏0
  • 浅谈布隆过滤器

    摘要:那该怎么办,现在该介绍今天的主角了布隆过滤器就可以解决这样的问题。具体介绍布隆过滤器实际上是一个很长的二进制矢量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。缺点布隆过滤器有宁可错杀一百,也不能放过一个的性质。 1. 问题情景 如果面试官问你,一个网站有 100 亿 url 存在一个黑名单中,每条 url 平均 64 字节。问这个黑名单要怎么存?若此时随便输入一...

    jone5679 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<