在 JS 中的位运算都是针对整数而言的,只要进行位运算,JS 统统会将数字看做 32 位的补码进行运算。
至于什么是补码,这里并不过多介绍具体细节,读者只需要知道补码的运算规则,首先,将数字直接转换为二进制,其中第一位为符号位,0 表示正数,1 表示负数。我们称这样直接转换而来的二进制为原码,如果这个数字是一个正数,那么原码和补码相同。如果这个数是负数,则其补码运算规则为:除符号位以外,其余位置按位取反,末尾加一。关于补码的细节不过多赘述,但也有一些需要注意的地方,首先,引入补码主要解决了减法运算,可以把所有减法运算都变成加法运算,同时解决了 0 的不唯一性问题,至于为什么有这些好处,读者有兴趣可以自行了解。补码有一些常见的性质,x 的补码,再求一次补码,得到的就是它本身,这个性质非常好,例如 x 和 y,我们分别求得补码 x'和 y',然后直接进行加法运算,即 x'+y',得到 z',此时得到的结果还是补码,然后我对这个补码再求一次补码,就还原为了 z,此时 z 就是 x+y 的结果。读者了解到这个层次已经足够了。
如果是小数进行位运算,则需要先转换为整数,转换方式为截断,或者说直接舍弃。
对于 NaN,Infinity,这些特别的数字,在进行位运算的时候,全部当做 0
首先第一个是位与运算,前面介绍过与运算,也就是 &&,而按位与则是 &,将两个整数按位与,即将他们先看做 32 位的整数,然后按位置一位一位进行与,得到一个最终数字,0 看做 false,1 看做 true。
这里举个例子,52.5&98,这个结果是多少,首先,将这里两个数字转换为 32 位二进制,52.5 由于是小数,会先截断为整数,也就是 52,而 52 转为 32 位二进制为 0000 0000 0000 0000 0000 0000 0011 0100,而-98 转化为 32 位二进制为 000 0000 0000 0000 0000 0000 0110 0010。
0000 0000 0000 0000 0000 0000 0011 01000000 0000 0000 0000 0000 0000 0110 00100000 0000 0000 0000 0000 0000 0010 0000
此时将这两个数一位一位按位与,如上所示,按位与的规则就是与,将 1 看为 true,0 看做 false。例如最高位是 1 和 0 与,结果就是 0,这样依次按位与,得到的结果如下:0000 0000 0000 0000 0000 0000 0001 0100
接下来,我们考虑 52&-98,52 是正数,原码和补码一样,而-98 是负数,原码和补码不一样,先转换为原码,结果为 1000 0000 0000 0000 0000 0000 0110 0010,接下来求补码,除了符号位之外,其余位按位取反,末尾加一,结果为:1111 1111 1111 1111 1111 1111 1001 1111
0000 0000 0000 0000 0000 0000 0011 01001111 1111 1111 1111 1111 1111 1001 11110000 0000 0000 0000 0000 0000 0001 0100
按位与运算之后,得到的结果也是补码,我们还要再求一次补码,转换为原码,但是我们观察,这个补码是一个正数,第一位的符号位为 0。而对于正数,原码和补码相同,所以最终结果就是它自己,转换为十进制后,得到为 20。
如果读者理解了按位与运算,那么按位或运算我相信不需要介绍,按位或运算的运算符为 |,x|y 也是将 x 和 y 分别转换为 32 位二进制补码,然后一位一位进行或运算,得到的结果还是一个补码,再求补转换为原码进行返回。同时,按位取反也类似,符号位 ~,这是一个一元运算符,对应非运算,即 ~x 就是将 x 转换为补码,再将其按位取反,得到的结果再转为原码即可。
取反的快速运算:
~x的结果为-x-1。这个公式可用于速算。
其实不管在什么编程语言里,位运算都是效率相当高的运算,JS 也不例外,由于位运算天然会将数字截断为整数,所以我们其实可以通过
~~x对 x 进行快速截断取整,即直接舍弃掉小数位。由于位运算执行效率相当高,所以这也是 JS 中最快的取整方式。
接下来是一个比较陌生的位运算,按位异或,符号为 ^,x^y 即将 x 和 y 都转换为二进制补码,每一位进行异或运算,异或运算的规则为:相同为 false,不同为 true。
这里提供的是按位异或,在 JS 中没有提供逻辑异或的功能,我们可以使用已经提供的逻辑与或非运算,来自行实现一个异或运算,x 和 y 的逻辑异可以表达为
(!x && y)||(x && !y)
异或运算可以用于交换律两个整数变量
js
var a = 1,b = 2;//接下来使用三种方式交换两个变量
//方式1
var temp = a;
a = b;
b = temp;
//方式2
a = a + b;
b = a - b;
a = a - b;
//方式3
a = a ^ b;
b = a ^ b;
a = a ^ b;异或运算交换两个数的原理,其实是利用了异或运算的一些性质,异或运算具有交换律,结合律,异或 1 相当于取反,异或 0 相当于不变,自己和自己异或得到的是全 0,掌握这些性质后,读者可以尝试自行分析其原理。
接下来是移位运算,分别是左移运算:<<,算术右移:>>,无符号右移 :>>>
首先是左移,左移很简单,x<<y 表示数字 x 的补码的符号位固定,其余位置向左移动 y 位,右侧移入的位置用 0 填充。
这里以 8 位为例,32 位同理

第一位绿色的为符号位,我们左移一位,结果如下

可以看到,相当于就是数值位整体往左挪了一位,其中红色的 1 由于是第一位,往左一挪就被舍弃掉了,挪完之后最右边又空出一位,统一用 0 填充即可。
接下来是算术右移,算术右移和算术左移是类似的,但是有一点区别,就是最高一位填充的问题,看下面的例子

我们进行一位的算术右移

我们看结果,我们发现,最低一位一样是因为移出被舍弃了,但是最高位的填充却不是统一用 0 填充,实际上,最高位的填充取决于符号位,如果符号位为 1,那么用 1 填充,如果符号位为 0,用 0 填充,这里符号位为 1,所以就用 1 填充。
最后就是无符号右移,无符号右移就很简单了,就是完全不看符号位,直接带着符号位整体右移,移出的部分舍去,空出来的部分用 0 填充。这里不再赘述。
从数学上,左移一位相当于乘以 2,右移一位相当于除以 2 之后向下取整,由于位运算非常快,所以当我们需要进行乘以 2 或者除以 2 的操作,这是最快速的方式。当然,由于在 JS 中,位运算用的是 32 位补码,所以表示范围为 ,也就是正负四十二亿左右,虽然和上一节中介绍的最大安全数字差距还很大,但是也足够我们使用。