JS位运算指南

165 阅读6分钟

提出问题

1、1 << 31 2 ** 31 值分别为多少

2、~0 ~-1值分别为多少,怎么计算的

3、(2 ** 31 - 1) << 1(2 ** 31 + 1) << 1 分别为多少

4、(2 ** 31 - 1) & -1(2 ** 31 + 1) & -1为多少,是否等于 -1 >>> 0

如果上述例子都知道结果和原因,那么本文就不需要往下读了。


规范

之所以疑惑上述例子,其实是因为两个规范没理解

1. ECMA-262规定所有的数进行位运算都会先转换成有符号32位整数(-2 ** 31 ~ 2 ** 31 - 1)后、再进行运算

image.png

2. 在计算机中,数值均用补码的形式来存储 (个人理解:二进制数据都是以补码的形式存储和计算,与十进制进行转换需要使用原码)


依照着这两条规范、现在再来解答上述例子:

1、1 << 31、位运算参数和返回值都必须是有符号32位整数:得到的结果是10000000000000000000000000000000,最高位的1为符号位, 理论上应该是-0, 但是有符号32位整数范围是-2147483648 ~ 2147483647,因此结果应该是-21474836482 ** 31 没有进行位运算,无需转化成有符号32位整数,因此结果为2147483648

2、~-1,进行了位运算,首先转化成有符号32位整数:注意,负数在计算机中以补码的形式存在,具体的计算规则是先转化成反码10000000000000000000000000000001(原码) => 111111111111111111111111111111110(反码) => 11111111111111111111111111111111(补码)~运算符是按位取反,也就是每一位都取反,因此得到的最终结果为00000000000000000000000000000000,有符号32位,计算出来为0。

3、2 ** 31 - 1 转化成有符号32位整数为: 01111111111111111111111111111111 、这里给大家推荐一个十进制转换二进制的小技巧,可以直接在浏览器控制台通过toString(2)方法转换,如下图:

image.png

左移一位后变成了 11111111111111111111111111111110,注意,此时是补码,最高为1表示是负数(如果是正数无需处理,正数的补码等于其自身),需要计算其原码然后再转化为十进制11111111111111111111111111111110(补码) => 11111111111111111111111111111101(反码) => 10000000000000000000000000000010(原码),最终得到十进制结果为-2

4、(2 ** 31 - 1) & -1需要将两个数都转化成有符号32位整数后进行运算:-1 为负数需要计算补码:10000000000000000000000000000001(原码) => 11111111111111111111111111111110(反码) => 11111111111111111111111111111111(补码),和01111111111111111111111111111111按位与后得到了01111111111111111111111111111111,也就是2147483647

(2 ** 31 + 1) 的二进制数据为 10000000000000000000000000000001, 和-1按位与后还是自身,但此时是负数了,需要计算原码,最后得到的结果是-2147483647

这里需要介绍下无符号右移动>>>,先看规范:

image.png

简单来说,无符号右移也是32位整数,且都是正数嘛,最高位肯定是0,所以右移最高位补0

注意进行运算前需要把两个数都转化成无符号 ToUnit(32),我们看看规范怎么描述的:

image.png 本质上就是最高位不当符号处理,正常使用二进制计算。 所以 -1 >>> 0 等同于将-1置为无符号,也就是-1的补码11111111111111111111111111111111当无符号32位处理,即2 ** 32 - 1 = 4294967295

另外任意一个数a(整数/小数), a | 0 等价于 a >> 0a << 0,通常用来取整。


很酷的应用

所以位运算到底有什么用呢,下面隆重介绍一下用位运算实现一个Math.random

首先需要介绍Math.imul,先看官方介绍

image.png

简单来说,我们实现一个随机数,可以用一个seed乘以一个质数,不停地相乘。但是要保证不越界,一直在某个范围内,这时候就可以使用Math.imul(本质上就是(a | 0) * (b | 0)),因为进行了位运算就可以实现在(-2 ** 31 ~ 2 ** 31 - 1)

剩下就只需要解决一个问题:负数怎么处理?因为Math.random返回的是[0~1)的随机数。我们可以把结果按位 & (2 ** 31 - 1), 这样就能保证符号位肯定为正了,下面展示代码:

class Random {
     constructor(seed) {
         this.seed = seed;
     }
     next() {
        if (this.seed) {
              return ((2 ** 31 - 1) & (this.seed = Math.imul(48271, this.seed))) / 2 ** 31;
         }
         else {
              return Math.random();
          }
     }
 }

思考一下 如果不按位 & (2 ** 31 - 1),而是使用 >>>,结果会是一样的吗?

最后,位运算在算法中有着不可替代的作用,可以让代码变得非常简洁明了。我收集了几道位运算相关的题目,大家可以抽空了解下。