浅谈JavaScript位操作符

夕水

位操作符的基本概念

因为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()方法指定参数可以表示将一个十进制数转换成二进制数.所以我在这里写了一个函数,表示将一个十进制数转换成二进制数,如下图所示:

clipboard.png

既然二进制数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是会尽力向我们隐藏所有的这些信息.也就是说在实际转换负数的二进制码时,它只会将这个负数的绝对值的二进制码前面加上一个负号,就表示这个负数的二进制码.如下图所示:

clipboard.png

这个转换过程说明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.要理清这个转换过程,需要知道什么是反码,什么是原码,什么又是补码,因为参与计算的是补码,而要转换的是求原码.也就是说,要想将二进制反码转换成十进制数,就必须求得二进制反码的原码,然后对原码直接按照二进制转换成十进制的方式来计算转换.现在我们来验证一下是否是我们所想的,如下图所示:

clipboard.png

再比如求-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.如下图所示:

clipboard.png

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

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

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

clipboard.png

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

clipboard.png

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

b.按位与(AND)

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

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

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

如以下示例:

clipboard.png

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,现在咱们来验证一下,如下图所示:

clipboard.png

c.按位或(OR)

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

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

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

clipboard.png

现在,我们就来分析一下为什么结果是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求结果如下图:

clipboard.png

现在来分析一下为什么结果是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来填充这些空位.

如以下示例:

clipboard.png

现在来分析一下为什么结果是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.

如以下一个示例:

clipboard.png

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

再来看一个示例:

clipboard.png

现在,咱们就来分析分析为什么最终结果为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.现在咱们来操作验证一下,如下图:

clipboard.png

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

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

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

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

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

clipboard.png

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

clipboard.png

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

clipboard.png

前端面试题分析

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

clipboard.png

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

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

clipboard.png

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

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

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

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

clipboard.png

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

clipboard.png

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

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

clipboard.png

通过使用按位与操作符取得非负整数与非负整数减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.我们可以直接调用这个函数,如下图所示:

clipboard.png

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

阅读 691

eveningwater
每天学习一点点,就可以进步一点点,工作能带来的不仅是技术知识点,还有与人的相处,沟通与交流。这是...

问之以是非而观其志,穷之以辞辩而观其变,资之以计谋而观其识,告知以祸难而观其勇,醉之以酒而观其性,临之以利而观其廉,期之以事而观其信。

2.9k 声望
192 粉丝
0 条评论
你知道吗?

问之以是非而观其志,穷之以辞辩而观其变,资之以计谋而观其识,告知以祸难而观其勇,醉之以酒而观其性,临之以利而观其廉,期之以事而观其信。

2.9k 声望
192 粉丝
宣传栏