4

image

按位操作符

JavaScript中使用IEEE-754 64位存储。位操作符不能直接操作64位的值,而是将它转换为二进制补码形式的32位的整数,最后再将结果转为64位。32位中31位表示整数的值,第32位为符号位(0为正数,1为负数)。每一位由二进制数存储,31位中的每一位的索引表示2的次幂乘与每一位的0或者1。没有使用到的位将使用0填充。

举一个例子🌰。31的32位二进制数表示为0000 0000 0000 0000 0000 0000 0001 1111。第32位0表示符号位,11111为有效位。

1 1 1 1 1
2^4 * 1 2^3 * 1 2^2 * 1 2^1 * 1 2^0 * 1
16 8 4 2 1

负数

负数使用二进制存储,但是使用二进制补码表示

如何求一个数的二进制补码?

    1. 求出该数绝对值的二进制码
    1. 将获得的二进制码取反(1变成0,0变成1)
    1. 二进制码加1

举一个例子🌰。如何求出-31的二进制补码。-31的二进制补码的结果为1111 1111 1111 1111 1111 1111 1110 0001。

  • -31的绝对值为31,求出31的二进制的码
0000 0000 0000 0000 0000 0000 0001 1111
  • 将31的二进制码的0和1颠倒
1111 1111 1111 1111 1111 1111 1110 0000
  • 将二进制码加1
1111 1111 1111 1111 1111 1111 1110 0000
                                     +1
---------------------------------------
1111 1111 1111 1111 1111 1111 1110 0001

无符号数

ECMAScript所有整数都是有符号位的。在无符号数中,第32位不表示符号,因为无符号数只能是正数,32位无符号数可以表示更大的数。

特殊值的处理

在NaN和Infinity在位运算符中会被当作0处理。非数值类型会先使用Number()处理,然后再应用位操作符。

按位非(NOT)

按位非~会将数值的32位二进制的每一位取反(0变为1,1变为0)。按位非的操作符的本质取操作数的负值,然后减一

// -24
~23

// -101
~100

// 9
~-10

// 100
~-101

举一个例子🌰。10进行按位非之后的结果,等于-11


0000 0000 0000 0000 0000 0000 0000 1010
// ~ NOT
1111 1111 1111 1111 1111 1111 1111 0101

按位与(AND)

按位与&, 本质上将两个操作数的32位二进制数的每一位对齐。然后按如下的规则取值,1 & 1 等于 1; 1 & 0 等于 0;0 & 1 等于0;0 & 0等于0。

举一个例子🌰。10和5之间进行按位与操作的结果,等于0


0000 0000 0000 0000 0000 0000 0000 1010
// & AND
0000 0000 0000 0000 0000 0000 0000 0101
// 等于
0000 0000 0000 0000 0000 0000 0000 0000

按位或(OR)

按位与|, 本质上将两个操作数的32位二进制数的每一位对齐。然后按如下的规则取值,1 | 1 等于 1; 1 | 0 等于 1;0 | 1 等于1;0 | 0等于0。

举一个例子🌰。10和5之间进行按位或操作的结果,等于15

0000 0000 0000 0000 0000 0000 0000 1010
// & OR
0000 0000 0000 0000 0000 0000 0000 0101
// 等于
0000 0000 0000 0000 0000 0000 0000 1111

按位异或(XOR)

按位异或^, 本质上将两个操作数的32位二进制数的每一位对齐。然后按如下的规则取值,1 ^ 1 等于 0; 1 ^ 0 等于 1;0 ^ 1 等于1;0 ^ 0等于0。

举一个例子🌰。10和5之间进行按位异或操作的结果15。

0000 0000 0000 0000 0000 0000 0000 1010
// ^ XOR
0000 0000 0000 0000 0000 0000 0000 0101
// 等于
0000 0000 0000 0000 0000 0000 0000 1111

左移

左移(<<)将32位二进制向左移动指定的位数,空缺的位将会使用0填充。左移不会影响符号位

举一个例子🌰。将5左移2位,等于20


0|000 0000 0000 0000 0000 0000 0000 0101
// <<2
0|000 0000 0000 0000 0000 0000 0001 0100
// 等于
(2^0 * 0) + (2^1 * 0) + (2^2 * 1) + (2^3 * 0) + (2^4 * 1) = 0 + 0 + 4 + 0 + 16 = 20

有符号位右移

右移(>>)将32位二进制向右移动指定的位数,但是保留符号位,右移空缺的符号位使用0填充

举一个例子🌰。将31有符号右移3位,等于3。

0|000 0000 0000 0000 0000 0000 0001 1111
// >>3
0|000 0000 0000 0000 0000 0000 0000 0011
// 等于
(2^0 * 1) + (2^1 * 1) = 1 + 2 = 3

无符号位右移

无符号位右移,会将所有32位数都向右移动。对于正数来说右移和无符号位右移的结果是一致的。

举一个例子🌰。将-31无符号右移28位。


1111 1111 1111 1111 1111 1111 1110 0001
// >>> 28
0000 0000 0000 0000 0000 0000 0000 1111
// 等于
(2^0 * 1) + (2^1 * 1) + (2^2 * 1) + (2^3 * 1) = 1 + 2 + 4 + 8 = 15

位操作符的奇妙用法

~~ 向下取整

按位非~。将操作数取负值,并减1。对于浮点数,会直接舍弃小数的部分。比如~11.1 === -12。然后将结果,再次执行按位非操作,就会得到了去除小数位的原数字,~-12 === 11。因此我们可以使用~~代替Math.floor。

// 12
~~12.5

// 6
~~6.1

// 8
~~-8.1

Math.floor和~~性能对比

我分别使用和Math.floor,对包含了10000000个浮点数的数组,进行向下取整。耗时386毫秒,Math.floor耗时411毫秒.

~ 判断索引是否存在

  • ~-1等于0(-1取正值后减1)

平时使用indexOf等API时,当查询的字符串不存在时,indexOf会返回-1。但是Boolean(-1)等于true,所以我们在使用时通常需要添加一个判断'字符串'.indexOf('字') > -1,看上去并不是那么的简洁。
-1按位非等于0,而Boolean(0)是等于false的。所以我们可以把不等式简化成下面的样子。


if ('米莉波比布朗'.indexOf('莉') > -1) {
}

// 等价于
if (~'米莉波比布朗'.indexOf('莉')) {
}

^ 不使用额外的空间交换两个变量的值

在了解这个技巧之前需要先了解两个准则:

  1. 两个相同的数进行按位异或等于0
  2. 任意一个数与0进行按位异或等于自身

// 0
100 ^ 100
// 0
-99 ^ -99

// 100
100 ^ 0
// -2
0 ^ -2

在代码中交换两个变量的值,通常是通过第三个变量,或者使用ES6中的解构赋值

// 使用第三个变量
var a = 1
var b = 2
var c = a
a = b
b = c

// 使用解构赋值
var x = 1
var y = 2
[x, y] = [y, x]

使用异或按位运算符

let a = 2
let b = 3

a = a ^ b
// b = a ^ b ^ b => a ^ (b ^ b) => a ^ 0 = a
b = a ^ b
// a = a ^ b ^ a ^ b ^ b => (a ^ a) ^ (b ^ b) ^ b => 0 ^ 0 ^ b => 0 ^ b = b
a = a ^ b

^ 切换位

使用异或按位运算符,可以用来切换二进制数中的某些位,这基于这样一个事实:

  1. 任意一个数与0进行按位异或等于自身(上面提到过)
  2. 任意一个数与与1进行按位异或,总是被切换(参考下面的例子)

let a = 1
// 0
a = 1 ^ 1
// 1 (又变回了1)
a = a ^ 1

举一个例子🌰,let a = 0b101011, 我们想将中间的四位进行切换(0->1, 1->0)变为0b110101。我们创建一个掩码,首尾为0,中间四位为1, 使用掩码进行按位异或的操作。

// 掩码
let mask = 0b011110
let a = 0b101011

// 0b110101
a ^ mask

& 关闭高位

使用按位与&, 可以用来关闭二进制数中的某些位,这基于这样一个事实:

  1. N & 0 === 0 任意一个数与0,按位与,等于0
  2. N & N === N 任意一个数与自身,按位与,等于自身
  3. N & -N === 0 任意一个数与自身负值,按位与,等于0
  4. N & -1 === N 任意一个数与-1,按位与,等于自身

比如我们有一个8位的二进制数,我们需要关闭后4位(设置为0),我们首先创建一个8位的位掩码,位掩码的后四位设置为0,前四位设置为1。接下来使用位掩码与操作数进行按位与操作


const mask = 0b00001111

// 结果00001010,成功将后四位设置为0
mask & 0b10101010

& 检查设置位

可以使用按位与,检查二进制数中某一位是否设置了。

举一个例子🌰,我们需要检查第五位是否有数字。

const mask = 0b10000

// false
0b100010 & mask === mask

// true
0b110010 & mask) === mask)

& 判断奇数或偶数

奇数与偶数的二进制表示的特性:

  • 偶数的二进制的第一位始终是0
  • 奇数的二进制的第一位始终是1

// 0b0
0
// 0b1
1
// 0b10
2
// 0b11
3
// 0b100
4
// 0b101
5
// 0b110
6

所以我们可以使用0001作为掩码,使用0关闭高位,只保留第一位。当number & 1 === 1, number为奇数。number & 1 === 0, number为偶数。

  • 偶数与0b1,按位与,结果必定等于0
  • 奇数与0b1,按位与,结果必定等于1
// 奇数
(3 & 1) === 1

// 偶数
(4 & 1) === 0

<<,>> 移位实现乘除法

ps:😂在工程中,最好不要这样写,免得的被打。

左移1位,等于乘以2,左移2位,等于乘以4。但是对于乘以7或者乘以9该怎么办呢?乘以7,可以分解为左移3位并减去一位,a * 7 === (a << 3) - a。乘以9,可以分解为左移3位并加上一位a * 9 === (a << 3) + a
  • 左移1位,等价于,乘以2的1次方。
  • 左移2位,等价于,乘以2的2次方。
  • 左移3位,等价于,乘以2的3次方。
  • ...

a * 2 === a << 1
a * 3 === (a << 1) + a
a * 4 === a << 2
  • 右移1位,等价于,除以2的1次方。
  • 右移2位,等价于,除以2的2次方。
  • 右移3位,等价于,除以2的3次方。
  • ...
a / 2 === a >> 1
a / 4 === a >> 2
a / 8 === a >> 3

利用位操作符,解答leetcode

从这些题目中,可以get到的点

  1. 2的n次幂,二进制中只有一个1。2的n次幂与2的n次幂减一,按位与,结果永远是0。
  2. 可以使用>>>右移代替除法
  3. 可以使用二进制的0和1,模拟有或无
  4. 异或可以用来实现简单的去重
  5. 异或配合按位与可以实现加法

参考


已注销
518 声望187 粉丝

想暴富