现代JavaScript编程:经典范例与实践技巧
上QQ阅读APP看书,第一时间看更新

2-16 位运算符

我们知道,程序中的所有数在计算机内存中都是以二进制的形式存储的。这很好理解,进制的实质是确定计数时逢几进一。人类有10只手指,因此很久以前,祖先就习惯了使用十进制来计数。计算机的核心是由电子元件组成的,而电子元件最容易描述的两种状态是高电平与低电平,因此使用二进制计数是最安全、最便捷的方式。

在介绍ECMAScript中的位运算前,先来简单地了解一下JavaScript中二进制计数。JavaScript中只有一种数值类型:原始类型Number。但是实际上,JavaScript中存储的数值有两种,分别为有符号数和无符号数。其实这和大多数编程语言类似,只是有些强类型的语言会将数值类型再进行细化,比如8位整型、32位整型、64位整型、32位浮点型或64位浮点型。JavaScript中所有的数值默认都是32位的(当然,这样说并不准确,具体的位数和计算机环境有关,目前大多都是32位的)。位数即表示一个数字需要多少个二进制位,我们暂定JavaScript中所有的数值都是32位的,那么8这个十进制数在内存中存储的数据如下:

上面每一个方格表示一个二进制位,中间的省略号代表省略中间的0,一共32个小方格,代表32个数位。

JavaScript中所有的数值创建时默认都是有符号的,虽然存储一个数值需要32位,我们能够操作的实际上只有31位,最后一位作为符号位,符号位为0表示这个数值是正数,符号位为1表示这个数值是负数。对于正数,存储在内存中的二进制数据很好理解,将此正数的二进制形式放入内存,其余位补零即可。但是对于负数,其在内存中是以二进制补码方式存储的,计算补码的步骤如下:

(1)确定该数的绝对值的二进制形式。

(2)对此二进制码求反码(0和1互相交替)。

(3)在反码的基础上加1。

根据上面的规则,以十进制数-8为例,其绝对值的二进制形式为0…01000,对其求反码为1…10111,在其基础上再加1得到1…11000,即十进制数-8实际存在内存中的数据如下:

相对于有符号数而言,无符号数中并没有负数,所有的数值都是正数,在这种情况下,正负位就失去了作用,因此对于无符号数来说,其32个二进制位都用来表示数字。

额外说一点,计算机中为什么要采用补码的方式来存储数据呢?对于有符号数,最高位表示的是符号,如果直接进行二进制形式的存储,难免会出现这样一种情况:0可以表示为正数0和负数0,这有悖现实规律。因此,人们采用补码的方式使现实的数值与计算机内存中存储的二进制数据一一对应,正数的补码是其本身,负数的补码是其反码加1。经过这样的计算后,无论是正数0还是负数0,在计算机内存中存储的都是全0码,做到了统一。

理解了计算机中的二进制计算原理,我们再来看位运算符。顾名思义,位运算就是在二进制位的基础上进行运算,其直接对二进制位进行操作。ECMAScript中支持的位运算有7种,分别为按位非运算、按位与运算、按位或运算、按位异或运算、按位左移运算、按位有符号右移运算和按位无符号右移运算。

按位非运算使用符号“~”来定义,它也被称为按位取反运算,即原二进制位是1的变为0,原二进制位是0的变为1。示例代码如下:

对8进行按位取反运算后,结果将为-9,如果将十进制换成二进制表示,这个过程就很好理解,首先8的二进制形式如下:

按位取反后如下:

前面说过,负数存储的实际上是补码,那么通过逆运算,先对补码减1,如下:

再对补码减1后得到的反码取反,如下:

上面的原码就是我们最终结果的绝对值形式,将其转换成十进制并且加上负号,就得到了-9。

在编程中,你并不需要对每一次按位取反操作都进行如上推演,上面介绍的过程只是原理,理解了原理后,我们可以通过技巧记忆的方式来快速得到想要的答案,即对数值的按位取反操作实际上就是将此数值求负再减1。

按位与运算使用“&”符号定义,是一个二元运算符,其进行运算的两个操作数的对应二进制位分别进行与运算后将结果返回,即如果进行运算的相应位都为1,最终结果数值的此二进制位为1,否则为0。示例代码如下:

分解上述代码的计算过程如下。

1的二进制码:

9的二进制码:

进行按位与运算后:

最终结果为1。

按位或运算使用“|”符号定义,是一个二元运算符,其进行运算的两个操作数的对应二进制位分别进行或运算后将结果返回,即如果进行运算的相应位都为0,最终结果的此二进制位为0,否则为1。示例代码如下:

分解上述代码的计算过程如下。

8的二进制码:

3的二进制码:

进行按位或运算后:

最终结果为11。

按位异或运算使用符号“^”定义,是一个二元运算符,其进行运算的两个操作数对应二进制位分别进行异或运算后将结果返回,即如果进行运算的相应位不同,最终结果数值的此二进制位为1,否则为0。示例代码如下:

分解上述代码的计算过程如下。

8的二进制码:

11的二进制码:

进行按位异或运算后:

最终结果为3。

按位左移运算使用符号“<<”定义,其作用是将二进制数据向左移动指定的位数,右侧空出来的位将进行补零操作。需要注意,按位左移操作并不会影响符号位,移动过程并不包括符号位,示例代码如下:

分解上述代码的计算过程如下。

-2的二进制码(补码):

进行左移两位的运算后:

对补码求原码:

最终结果为-8。

与按位左移运算相对应的还有按位右移运算。需要注意,按位右移运算有两种:有符号按位右移运算与无符号按位右移运算。其中,有符号按位右移运算与按位左移运算互为逆运算,使用“>>”符号定义,示例如下:

无符号按位右移运算和有符号按位右移运算最大的不同在于:无符号按位右移运算时,并不保留符号位,会将符号位一起进行移动,其使用“>>>”符号定义。正数的符号位为0,因此对正数并没有影响,负数就不同了,示例如下:

具体过程这里不再重复,你可以根据前面的示例自行推导一下。

因此,在使用无符号右移运算时要极其小心。