巧用 JS 位运算

位运算是直接对二进制位进行计算,它直接处理每一个比特位,是非常底层的运算,好处是速度极快,缺点是很不直观,许多场合不能够使用。
位运算只对整数起作用,如果一个运算数不是整数,会自动转为整数后再运行。
在JavaScript内部,数值都是以64位浮点数的形式储存,但是做位运算的时候,是以32位带符号的整数进行运算的,并且返回值也是一个32位带符号的整数。

运算符 用法 描述
按位与(AND) a & b 对于每一个比特位,只有两个操作数相应的比特位都是1时,结果才为1,否则为0。
按位或(OR) a | b 对于每一个比特位,当两个操作数相应的比特位至少有一个1时,结果为1,否则为0。
按位异或(XOR) a ^ b 对于每一个比特位,当两个操作数相应的比特位有且只有一个1时,结果为1,否则为0。
按位非(NOT) ~ a 反转操作数的比特位,即0变成1,1变成0。
左移(Left shift) a << b 将 a 的二进制形式向左移 b (< 32) 比特位,右边用0填充。
有符号右移 a >> b 将 a 的二进制表示向右移 b (< 32) 位,丢弃被移出的位。
无符号右移 a >>> b 对于每一个比特位,只有两个操作数相应的比特位都是1时,结果才为1,否则为0。

位运算是非常底层的运算,由于速度极快,借助位运算的特性还能实现一些算法,因此恰当地使用运算有很多好处。下面搜集了比较常用的例子:

类型转化

1
2
3
4
let myVar = "3.14";
str = "" + myVar; // 转化为 字符串
int = ~~myVar; // 转化为整数
bool = !!myVar; // 转化为布尔

使用~, >>, <<, >>>, |来取整

1
2
3
4
5
6
console.log(~~8.88)    // 8
console.log(8.88 >> 0) // 8
console.log(8.88 << 0) // 8
console.log(8.88 | 0) // 8
// >>>不可对负数取整
console.log(8.88 >>> 0) // 8

切换变量 0 或 1

1
2
3
4
5
6
7
8
9
10
11
12
/ 一般方法:
if (toggle) {
toggle = 0;
} else {
toggle = 1;
}

// 一般方法的简写:
togle = toggle ? 0 : 1;

// 使用位运算的方法:
toggle ^= 1;

使用&运算符判断一个数的奇偶

1
2
3
4
// 偶数 & 1 = 0
// 奇数 & 1 = 1
console.log(2 & 1) // 0
console.log(3 & 1) // 1

使用按位非 ~ 判断索引存在

这是一个很常用的技巧,如判断一个数是否在数组里面:

1
2
// 如果url含有?号,则后面拼上&符号,否则加上?号
url += ~url.indexOf("?") ? "&" : "?";

其中 ~ 满足-(X+1)这个规律。

使用 异或^ 交换两个数

交换两个整数的值,最直观的做法是借助一个临时变量:

1
2
3
4
5
6
let a = 5,
b = 6;
// 交换a, b的值
let c = a;
a = b;
b = c;

现在要求不能使用额外的变量或内容空间来交换两个整数的值。这个时候就得借助位运算,使用异或可以达到这个目的:

1
2
3
4
5
6
let a = 5,
b = 6;

a = a ^ b; // 1 式
b = a ^ b; // 2 式 b 等于 5
a = a ^ b; // 3 式 a 等于 6

这个是为什么呢?很简单,把1、2式:

1
2
a = a ^ b;
b = a ^ b;

连起来就等价于:

1
b = (a ^ b) ^ b = a ^ (b ^ b) = a ^ 0 = a;

同理连同第3式可得:

1
2
a = (a ^ b) ^ a  // 在执行第3式的时候b已经变成a了,而a是第1式的a ^ b
= a ^ a ^ b = 0 ^ b = b;

异或还经常被用于加密。

使用按位与&去掉高位

按位与有很多作用,其中一个就是去操作数的高位,只保留低位,例如有a, b两个数:

1
2
let a = 0b01000110; // 十进制为70
let b = 0b10000101; // 十进制为133

现在认为他们的高位是没用的,只有低4位是有用的,即最后面4位,为了比较a,b后4位的大小,可以这样比较:

1
a & 0b00001111 < b & 0b00001111 // true

a, b的前4位和0000与一下之后就都变成0了,而后四位和1111与一下之后还是原来的数。这个实际的作用是有一个数字它的前几位被当作A用途,而后几位被用当B用途,为了去掉前几位对B用途的影响,就可以这样与一下。

另外一个例子是子网掩码,假设现在我是网络管理员,我能够管理的IP地址是从192.168.1.0到192.168.1.255,即只能调配最后面8位。现在把这些IP地址分成6个子网,通过IP地址进行区分,由于6等于二进制的110,所以最后面8位的前3位用来表示子网,而后5位用来表示主机(即总的主机数的范围为00001 ~ 11111, 共30个)。当前网络的子网掩码取为255.255.255.111 00000即255.255.255.224,假设某台主机的IP地址为192.168.1.120,现在要知道它处于哪个子网,可以用它IP地址与子网掩码与一下:120 & 224 = 96 = 0b 011 00000,就知道它所在的子网为011即3号子网。

这个是保留高位去掉低位的例子,刚好与上面的例子相反。

使用&, >>, |来完成rgb值和16进制颜色值之间的转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 16进制颜色值转RGB
* @param {String} hex 16进制颜色字符串
* @return {String} RGB颜色字符串
*/
function hexToRGB(hex) {
var hexx = hex.replace('#', '0x')
var r = hexx >> 16
var g = hexx >> 8 & 0xff
var b = hexx & 0xff
return `rgb(${r}, ${g}, ${b})`
}

/**
* RGB颜色转16进制颜色
* @param {String} rgb RGB进制颜色字符串
* @return {String} 16进制颜色字符串
*/
function RGBToHex(rgb) {
var rgbArr = rgb.split(/[^\d]+/)
var color = rgbArr[1]<<16 | rgbArr[2]<<8 | rgbArr[3]
return '#'+ color.toString(16)
}
// -------------------------------------------------
hexToRGB('#ffffff') // 'rgb(255,255,255)'
RGBToHex('rgb(255,255,255)') // '#ffffff

使用按位与&进行标志位判断

现在有个后台管理系统,操作权限分为一级、二级、三级管理员,其中一级管理员拥有最高的权限,二、三级较低,有些操作只允许一、二级管理员操作,有些操作只允许一、三级管理员操作。现在已经登陆的某权限的用户要进行某个操作,要用怎样的数据结构能很方便地判断他能不能进行这个操作呢?

我们用位来表示管理权限,一级用第3位,二级用第2位,三级用第1位,即一级的权限表示为0b100 = 4,二级权限表示为0b010 = 2,三级权限表示为0b001 = 1。如果A操作只能由一级和二级操作,那么这个权限值表示为6 = 0b110,它和一级权限与一下:6 & 4 = 0b110 & 0b100 = 4,得到的值不为0,所以认为有权限,同理和二级权限与一下6 & 2 = 2也不为0,而与三级权限与一下6 & 1 = 0,所以三级没有权限。这里标志位的1表示打开,0表示关闭。

这样的好处在于,我们可以用一个数字,而不是一个数组来表示某个操作的权限集,同时在进行权限判断的时候也很方便。

使用按位|构造属性集

上面构造了一个权限的属性集,属性集的例子还有很多,例如我在《Google地图开发总结》里面就提到一个边界判断的例子——要在当前鼠标的位置往上弹一个悬浮框,但是当鼠标比较靠边的时候就会导致悬浮框超出边界了。

为此,需要做边界判断,总共有3种超出情况:右、上、左,并且可能会叠加,如鼠标在左上角的时候会导致左边和上面同时超出。需要记录超出的情况进行调整,用001表示右边超出,010表示上方超出,100表示左边超出,如下代码计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let postFlag = 0;
//右边超出
if(pos.right < maxLen) posFlag |= 1;
//上面超出
if(pos.top < maxLen) posFlag |= 2;
//左边超出
if(pos.left < maxLeftLen) posFlag |= 4;
//对超出的情况进行处理,代码略
switch(posFlag){
case 1: //右
case 2: //上
case 3: //右上
case 4: //左
case 6: //左上
}

如果左边和上面同时超出,那么通过或运算2 | 4 = 6,得到6 = 0b110. 就知道了超出的情况,这样的代码相对于在if里面写两个判断要好一些。

您的支持将鼓励我继续创作!