Skip to content

JS 位运算

JS 位运算

带着问题看位运算

为什么 -1 >>> 0 === 4294967295?在本文的最后会给出最终的答案。

如果你看完本文之后能做出这道题,那么相信你对位运算已经能够初步使用了。

首先接下来我们就来从位运算的基础概念开始讲起。

基础概念

  1. ECMAScript 中的所有数值在计算机内部都是以 IEEE 754 64 位格式存储的,但是我们的位操作并不是直接操作的这 64 位二进制数,而是操作的计算机内部帮我们将这 64 位二进制数转化后的 32 位二进制数,操作完之后计算机在将结果转化回 64 位进行存储。因此对于开发者而言,我们可见的就只有这 32 位二进制数;

  2. ECMAScript 整数有两种类型,即有符号整数(允许使用正整数和负整数)和无符号整数(只允许使用正整数)。在 ECMAScript 中,所有整数字面量默认都是有符号整数。不过在特殊情况下确实存在无符号整数;

  3. 对于无符号整数来说,最高位不表示正负符号,因为只有正值。正是因为无符号位整数比有符号位整数多了一位表示数字的位,因此表示的数值范围无符号位比有符号位整数大;

  4. 一个数在计算机中的二进制表示叫做这个数的机器数,机器数的左边称之为高位,右边称之为低位;

  5. 机器数是带符号位的,在计算机用一个数的最高位存放符号位(最左边的数),正数的符号位为 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

  6. 对于一个数,计算机要使用一定的编码方式进行存储。原码,反码,补码是机器存储一个具体数字的编码方式;

  7. 位运算只对整数起作用,如果一个运算子不是整数,会自动转为整数(在JS中是通过Number()进行转化,Number()转化为NaN,那么这里默认用0来作为转化结果进行运算)后再进行运算;

  8. 我们可以这样简单的理解为,在计算机底层存储和进行位运算的都是机器数的补码;

  9. 原码,反码,补码的区别:

    1. 原码是人脑最容易理解和计算的表示方式。就是符号位加上真值的绝对值,即用第一位表示符号,其余位表示值。比如如果是8位二进制:

      [+1]原 = 0000 0001
      [-1]原 = 1000 0001
    2. 反码通常是原码向补码转化的中间产物。正数的反码是其本身,负数的反码是在其原码的基础上,符号位不变,其余各个位取反。

      [+1] = [00000001]原 = [00000001]反
      [-1] = [10000001]原 = [11111110]反
    3. 补码通常是计算机底层真正存储和计算的二进制数编码格式。正数的补码就是其本身,负数的补码是在其原码的基础上,符号位不变, 其余各位取反,最后(末位)+1。(即在反码的基础上(末位)+1

      [+1] = [00000001]原 = [00000001]反 = [00000001]补
      [-1] = [10000001]原 = [11111110]反 = [11111111]补

    大概提一嘴:补码的出现就是为了解决机器数加减时中可能出现的符号位的问题,并且还能使得机器数的表示范围增加一个最低位,比如说:8位二进制,使用原码或反码表示的范围为[-127,+127],而使用补码表示的范围为[-128, 127]

    参考:https://www.cnblogs.com/goahead—linux/p/10904701.html。

  10. 总结一下下面位运算会用到的知识点:

    1. 正数的原码,反码,补码相同。负数的原码,反码,补码各不相同;
    2. 原码转化为补码需要取反然后末位 +1,补码转化为原码则是先末位 -1,然后取反。取反是忽略符号位的;
    3. 计算机进行位运算和存储的都是数字的补码形式,而当我们拿来展示或者用的时候,计算机底层将其转化为原码,然后我们再用。因为原码是人脑最容易理解和计算的表示方式,而补码是计算机底层最方便计算的方式;

位逻辑运算符

对于为逻辑运算符,符号位是要参与运算的

运算符含义举例
&按位与n1 & n2
|按位或n1 | n2
~按位非(取反)~n1
^按位异或n1 ^ n2
  1. & 按位与

    规则:按位与,遇 0 则为 0,全部为 1 才是 1

    比如:-1 & 1,如何计算呢,先将两者都转化为补码的表现形式,然后进行上述规则的运算:

    [+1]十进制 = [0000 0001]原 = [0000 0001]反 = [0000 0001]补
    [-1]十进制 = [1000 0001]原 = [1111 1110]反 = [1111 1111]补
    将补码进行上述规则运算, 结果为: [0000 0001]补
    后来我们使用时计算机将这个补码形式的结果转化为原码
    这里我们得知这个补码的符号位为 0 也就是代表正数, 正数的原码反码补码相同
    因此 [0000 0001]补 = [0000 0001]反 = [0000 0001]原 = [+1]十进制
    因此 -1 & 1 结果为 +1
  2. | 按位或

    规则:按位与,遇 1 则为 1,全部为 0 才是 0

    比如:-3 | 1,如何计算呢,先将两者都转化为补码的表现形式,然后进行上述规则的运算:

    [+1]十进制 = [0000 0001]原 = [0000 0001]反 = [0000 0001]补
    [-3]十进制 = [1000 0011]原 = [1111 1100]反 = [1111 1101]补
    将补码进行上述规则运算, 结果为: [1111 1101]补
    后来我们使用时计算机将这个补码形式的结果转化为原码
    这里我们得知这个补码的符号位为 1 也就是代表负数
    因此 [1111 1101]补 = [1111 1100]反 = [1000 0011]原 = [-3]十进制
    因此 -3 | 1 结果为 -3
  3. ~ 按位非(取反)

    规则:按位非(取反),1 变为 00 变为 1

    比如:~ -3,如何计算呢,先将两者都转化为补码的表现形式,然后进行上述规则的运算:

    [-3]十进制 = [1000 0011]原 = [1111 1100]反 = [1111 1101]补
    将补码进行上述规则运算, 结果为: [0000 0010]补
    后来我们使用时计算机将这个补码形式的结果转化为原码
    这里我们得知这个补码的符号位为 0 也就是代表正数, 正数的原码反码补码相同
    因此 [0000 0010]补 = [0000 0010]反 = [0000 0010]原 = [+2]十进制
    因此 ~ -3 结果为 +2
  4. ^ 按位异或

    规则:按位异或,相同则为 0, 不同则为 1

    比如:-3 ^ 8,如何计算呢,先将两者都转化为补码的表现形式,然后进行上述规则的运算:

    [-3]十进制 = [1000 0011]原 = [1111 1100]反 = [1111 1101]补
    [+8]十进制 = [0000 1000]原 = [0000 1000]反 = [0000 1000]补
    将补码进行上述规则运算, 结果为: [1111 0101]补
    后来我们使用时计算机将这个补码形式的结果转化为原码
    这里我们得知这个补码的符号位为 1 也就是代表负数
    因此 [1111 0101]补 = [1111 0100]反 = [1000 1011]原 = [-11]十进制
    因此 -3 ^ 8 结果为 -11

位移运算符

运算符含义举例
<<(有符号位)左移位n1 << 1
>>(有符号位)右移位n1 >> 1
>>>无符号位右移位n1 >>> 1
  1. << (有符号位)左移位

    规则:将 32 位二进制数整体左移,对空位补符号位,溢位直接舍去(也就是超出最高位的),还有一点要注意的是,左移会保留它所对应的符号位。

    比如:8 << 3,如何计算呢,先将两者都转化为补码的表现形式,然后进行上述规则的运算:

    [+8]十进制 = [0000 1000]原 = [0000 1000]反 = [0000 1000]补
    上面我们是简写的模式, 下面是32位完整版
    [0000 0000 0000 0000 0000 0000 0000 1000]补
    左移 3 位
    xxx 0000 0000 0000 0000 0000 0000 0100 0xxx
    最高位(最左边的)xxx为溢位, 直接舍去。右边的空位(xxx)直接补上符号位, 也就是补上 0
    [0000 0000 0000 0000 0000 0000 0100 0000]补
    这里我们只取 8 位
    [0100 0000]补
    这里我们得知这个补码的符号位为 0 也就是代表正数, 正数的原码反码补码相同
    因此 [0100 0000]补 = [0100 0000]反 = [0100 0000]原 = [+64]十进制
    因此 8 << 3 结果为 +64

    比如:-8 << 3 该如何计算呢?先将两者都转化为补码的表现形式,然后进行上述规则的运算:

    [-8]十进制 = [1000 1000]原 = [1111 0111]反 = [1111 1000]补
    上面我们是简写的模式, 下面是32位完整版
    [1111 1111 1111 1111 1111 1111 1111 1000]补
    左移 3 位
    xxx 1111 1111 1111 1111 1111 1111 1100 0xxx
    最高位(最左边的)xxx为溢位, 直接舍去。右边的空位(xxx)直接补上符号位, 也就是补上1
    [1111 1111 1111 1111 1111 1111 1100 0000]补
    这里我们得知这个补码的符号位为 1 也就是代表负数, 然后将上面的补码转换位下面的反码, 最后一位 -1
    [1111 1111 1111 1111 1111 1111 1011 1111]反
    将反码转化为原码, 除去符号位 全部取反
    [1000 0000 0000 0000 0000 0000 0100 0000]原
    因此结果为 [-64]十进制
    因此 -8 << 3 结果为 -64
  2. >> (有符号位)右移位

    规则:将 32 位二进制数整体右移,对空位补符号位。

    比如:-16 >> 3该如何计算呢?

    [-16]十进制 = [1001 0000]原 = [1110 1111]反 = [1111 0000]补
    上面我们是简写的模式, 下面是32位完整版
    [1111 1111 1111 1111 1111 1111 1111 0000]补
    右移 3 位
    xxx1 1111 1111 1111 1111 1111 1111 1110 xxx
    最高位(最左边的)xxx为空位, 补上符号位, 也就是补上1。右边的溢位(xxx)直接舍去
    [1111 1111 1111 1111 1111 1111 1111 1110]补
    这里我们得知这个补码的符号位为 1 也就是代表负数, 然后将上面的补码转换位下面的反码, 最后一位 -1
    [1111 1111 1111 1111 1111 1111 1111 1101]反
    将反码转化为原码, 除去符号位 全部取反
    [1000 0000 0000 0000 0000 0000 0000 0010]原
    因此结果为 [-2]十进制
    因此 -16 >> 3 结果为 -2
  3. >>> 无符号位右移位

    规则:将 32 位二进制数整体右移,对空位补 0

    注意点:对于无符号位操作来说,会在底层先将数字转化为 32无符号位整数,然后对无符号位整数进行操作,而且我们知道,无符号整数只能用来表示正整数。 由于其最高位不是符号位,因此其表示的值的范围也比有符号位整数更大。

    比如:-16 >>> 3该如何计算呢?

    [-16]
    原码: 1000 0000 0000 0000 0000 0000 0001 0000
    反码: 1111 1111 1111 1111 1111 1111 1110 1111
    补码: 1111 1111 1111 1111 1111 1111 1111 0000
    然后右移 3 位, 最高位(最左边的)xxx为空位, 也就是补上 0。右边的溢位(xxx)直接舍去
    xxx1 1111 1111 1111 1111 1111 1111 1110 xxx
    补码: 0001 1111 1111 1111 1111 1111 1111 1110
    对于无符号位右移而言, 操作的数会看做无符号数, 因此只有正数, 那么这个数的原码, 反码, 补码可以看做一样的
    反码: 0001 1111 1111 1111 1111 1111 1111 1110
    原码: 0001 1111 1111 1111 1111 1111 1111 1110
    因此结果为 [+536870910]十进制
    因此 -16 >>> 3 结果为 +536870910

    比如:-1 >>> 0该如何计算呢?(也就是文章开头我们提出的问题)

    [-1]
    原码: 1000 0000 0000 0000 0000 0000 0000 0001
    反码: 1111 1111 1111 1111 1111 1111 1111 1110
    补码: 1111 1111 1111 1111 1111 1111 1111 1111
    由于 >>> 会将操作的数看做无符号数, 因此不会将首位 1 看做负数, 因此原反补相同
    然后右移 0 位, 空位补 0
    补码: 1111 1111 1111 1111 1111 1111 1111 1111
    反码: 1111 1111 1111 1111 1111 1111 1111 1111
    原码: 1111 1111 1111 1111 1111 1111 1111 1111
    这里我们要特别注意这个数没有符号位,全部是数值位
    因此结果为 [+4294967295]十进制
    因此 -1 >>> 0 结果为 +4294967295 (破案了!!!)
  4. 总结:

    1. 常规来说:

      对于有符号位的操作而言:num1 << n ---> num1 * (2 ** n)

      对于有符号位的操作而言:num1 >> n ---> res = num1 / (2 ** n); res < 0 ? Math.ceil(res) : Math.Floor(res)

      对于无符号位的正数的操作而言:num1 >>> n ---> num1 / (2 ** n)

      对于无符号位的负数而言,进行无符号位右移,它的原码会被转化为它的补码对应的值

    2. 有符号位操作:

      1. 有符号位的操作底层会将数字先转化位 32 位有符号位整数,然后进行操作;
      2. 有符号位的操作对空位补符号位的数字(也就是最高位的数字);
      3. 溢出的位舍去;
    3. 无符号位操作:

      1. 无符号位操作底层会将数字先转化位 32 位无符号位整数(最高位不是符号位,而是数值位,因此结果只有非负整数, 也就因此其原码反码补码一样),然后进行操作。

        对于负数而言,进行无符号位右移,它的原码会被转化为它的补码,即使你右移 0 位, 对应的值也会发生变化

        比如 -n >>> 0 无符号右移 0 位,也会使值发生变化;

      2. 无符号位的操作对空位直接补 0

      3. 溢出的位舍去;

    4. 位运算对于非数字的操作,会先用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

https://segmentfault.com/a/1190000014613703