JS 位运算
JS 位运算
带着问题看位运算
为什么 -1 >>> 0 === 4294967295
?在本文的最后会给出最终的答案。
如果你看完本文之后能做出这道题,那么相信你对位运算已经能够初步使用了。
首先接下来我们就来从位运算的基础概念开始讲起。
基础概念
-
ECMAScript
中的所有数值在计算机内部都是以IEEE 754 64
位格式存储的,但是我们的位操作并不是直接操作的这64
位二进制数,而是操作的计算机内部帮我们将这64
位二进制数转化后的32
位二进制数,操作完之后计算机在将结果转化回64
位进行存储。因此对于开发者而言,我们可见的就只有这32
位二进制数; -
ECMAScript
整数有两种类型,即有符号整数(允许使用正整数和负整数)和无符号整数(只允许使用正整数)。在ECMAScript
中,所有整数字面量默认都是有符号整数。不过在特殊情况下确实存在无符号整数; -
对于无符号整数来说,最高位不表示正负符号,因为只有正值。正是因为无符号位整数比有符号位整数多了一位表示数字的位,因此表示的数值范围无符号位比有符号位整数大;
-
一个数在计算机中的二进制表示叫做这个数的机器数,机器数的左边称之为高位,右边称之为低位;
-
机器数是带符号位的,在计算机用一个数的最高位存放符号位(最左边的数),正数的符号位为
0
,负数的符号位为1
;比如,十进制中的数
+3
,二进制表示为:0000 0000 0000 0000 0000 0000 0000 0011
。如果是-3
,二进制表示为:1000 0000 0000 0000 0000 0000 0000 0011
。我们我们一般在使用中只会取有效位:+3
对应0011
,-3
对应1000 0011
。 -
对于一个数,计算机要使用一定的编码方式进行存储。原码,反码,补码是机器存储一个具体数字的编码方式;
-
位运算只对整数起作用,如果一个运算子不是整数,会自动转为整数(在
JS
中是通过Number()
进行转化,Number()
转化为NaN
,那么这里默认用0
来作为转化结果进行运算)后再进行运算; -
我们可以这样简单的理解为,在计算机底层存储和进行位运算的都是机器数的补码;
-
原码,反码,补码的区别:
-
原码是人脑最容易理解和计算的表示方式。就是符号位加上真值的绝对值,即用第一位表示符号,其余位表示值。比如如果是
8
位二进制: -
反码通常是原码向补码转化的中间产物。正数的反码是其本身,负数的反码是在其原码的基础上,符号位不变,其余各个位取反。
-
补码通常是计算机底层真正存储和计算的二进制数编码格式。正数的补码就是其本身,负数的补码是在其原码的基础上,符号位不变, 其余各位取反,最后(末位)
+1
。(即在反码的基础上(末位)+1
)
大概提一嘴:补码的出现就是为了解决机器数加减时中可能出现的符号位的问题,并且还能使得机器数的表示范围增加一个最低位,比如说:8位二进制,使用原码或反码表示的范围为
[-127,+127]
,而使用补码表示的范围为[-128, 127]
。 -
-
总结一下下面位运算会用到的知识点:
- 正数的原码,反码,补码相同。负数的原码,反码,补码各不相同;
- 原码转化为补码需要取反然后末位
+1
,补码转化为原码则是先末位-1
,然后取反。取反是忽略符号位的; - 计算机进行位运算和存储的都是数字的补码形式,而当我们拿来展示或者用的时候,计算机底层将其转化为原码,然后我们再用。因为原码是人脑最容易理解和计算的表示方式,而补码是计算机底层最方便计算的方式;
位逻辑运算符
对于为逻辑运算符,符号位是要参与运算的
运算符 | 含义 | 举例 |
---|---|---|
& | 按位与 | n1 & n2 |
| | 按位或 | n1 | n2 |
~ | 按位非(取反) | ~n1 |
^ | 按位异或 | n1 ^ n2 |
-
&
按位与规则:按位与,遇
0
则为0
,全部为1
才是1
。比如:
-1 & 1
,如何计算呢,先将两者都转化为补码的表现形式,然后进行上述规则的运算: -
|
按位或规则:按位与,遇
1
则为1
,全部为0
才是0
。比如:
-3 | 1
,如何计算呢,先将两者都转化为补码的表现形式,然后进行上述规则的运算: -
~
按位非(取反)规则:按位非(取反),
1
变为0
,0
变为1
。比如:
~ -3
,如何计算呢,先将两者都转化为补码的表现形式,然后进行上述规则的运算: -
^
按位异或规则:按位异或,相同则为
0
, 不同则为1
。比如:
-3 ^ 8
,如何计算呢,先将两者都转化为补码的表现形式,然后进行上述规则的运算:
位移运算符
运算符 | 含义 | 举例 |
---|---|---|
<< | (有符号位)左移位 | n1 << 1 |
>> | (有符号位)右移位 | n1 >> 1 |
>>> | 无符号位右移位 | n1 >>> 1 |
-
<<
(有符号位)左移位规则:将
32
位二进制数整体左移,对空位补符号位,溢位直接舍去(也就是超出最高位的),还有一点要注意的是,左移会保留它所对应的符号位。比如:
8 << 3
,如何计算呢,先将两者都转化为补码的表现形式,然后进行上述规则的运算:比如:
-8 << 3
该如何计算呢?先将两者都转化为补码的表现形式,然后进行上述规则的运算: -
>>
(有符号位)右移位规则:将
32
位二进制数整体右移,对空位补符号位。比如:
-16 >> 3
该如何计算呢? -
>>>
无符号位右移位规则:将
32
位二进制数整体右移,对空位补0
。注意点:对于无符号位操作来说,会在底层先将数字转化为
32
位无符号位整数,然后对无符号位整数进行操作,而且我们知道,无符号整数只能用来表示正整数。 由于其最高位不是符号位,因此其表示的值的范围也比有符号位整数更大。比如:
-16 >>> 3
该如何计算呢?比如:
-1 >>> 0
该如何计算呢?(也就是文章开头我们提出的问题) -
总结:
-
常规来说:
对于有符号位的操作而言:
num1 << n ---> num1 * (2 ** n)
;对于有符号位的操作而言:
num1 >> n ---> res = num1 / (2 ** n); res < 0 ? Math.ceil(res) : Math.Floor(res)
;对于无符号位的正数的操作而言:
num1 >>> n ---> num1 / (2 ** n)
;对于无符号位的负数而言,进行无符号位右移,它的原码会被转化为它的补码对应的值
-
有符号位操作:
- 有符号位的操作底层会将数字先转化位
32
位有符号位整数,然后进行操作; - 有符号位的操作对空位补符号位的数字(也就是最高位的数字);
- 溢出的位舍去;
- 有符号位的操作底层会将数字先转化位
-
无符号位操作:
-
无符号位操作底层会将数字先转化位
32
位无符号位整数(最高位不是符号位,而是数值位,因此结果只有非负整数, 也就因此其原码反码补码一样),然后进行操作。对于负数而言,进行无符号位右移,它的原码会被转化为它的补码,即使你右移 0 位, 对应的值也会发生变化
比如
-n >>> 0
无符号右移0
位,也会使值发生变化; -
无符号位的操作对空位直接补
0
; -
溢出的位舍去;
-
-
位运算对于非数字的操作,会先用
Number()
将参与运算的运算子转化为数字,如果转化结果为NaN
, 那么运算子会被转化为0
,然后进入运算。比如:
('a' --> 0) >>> 1 === 0; ('a' --> 0) >> 2 === 0; ('a' --> 0) | ('a' --> 0) === 0...
;
-
参考链接
https://baike.baidu.com/item/%E6%97%A0%E7%AC%A6%E5%8F%B7%E6%95%B4%E6%95%B0/9203544?fr=aladdin