JavaScript中的位操做符

按位操做符

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

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

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

负数

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

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

    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位无符号数能够表示更大的数。spa

特殊值的处理

在NaN和Infinity在位运算符中会被看成0处理。非数值类型会先使用Number()处理,而后再应用位操做符。设计

按位非(NOT)

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

// -24
~23

// -101
~100

// 9-10

// 100-101
复制代码

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

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。code

举一个例子🌰。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. 异或配合按位与能够实现加法

参考

相关文章
相关标签/搜索