JavaScript 中的按位操作符
文章目录
文章目录
前言
JavaScript
提供了多种按位操作符,由于不是很了解,我使用频率很低。不过经常看到别人的代码中利用按位操作符简化代码,在一些场景下能够更有效率。本文记录学习按位操作符的笔记。
按位操作符
JavaScript
中的按位操作符见下表:
运算符 | 用法 | 描述 |
---|---|---|
按位与( AND ) |
a & b |
对于每一个比特位,只有两个操作数相应的比特位都是 1 时,结果才为 1 ,否则为 0 。 |
按位或(OR ) |
a | b |
对于每一个比特位,当两个操作数相应的比特位至少有一个 1 时,结果为 1 ,否则为 0 。 |
按位异或(XOR ) |
a ^ |
对于每一个比特位,当两个操作数相应的比特位有且只有一个 1 时,结果为 1 ,否则为 0 。 |
按位非(NOT ) |
~ a |
反转操作数的比特位,即 0 变成 1 ,1 变成 0 。 |
左移(Left shift ) |
a << b |
将 a 的二进制形式向左移 b (< 32) 比特位,右边用 0 填充。 |
有符号右移 | a >> b |
将 a 的二进制表示向右移 b (< 32) 位,丢弃被移出的位。 |
无符号右移 | a >>> b |
将 a 的二进制表示向右移 b (< 32) 位,丢弃被移出的位,并使用 0 在左侧填充。 |
所有的按位操作符的操作数都会被转成补码形式的有符号 32
位二进制整数。关于补码的知识,可以参考我的另一片文章原码,反码和补码。如果操作数是一个非数字,我们需要注意 JavaScript
会将它转化为什么:
Number(null) //0
Number(undefined) //NaN
Number([]) //0
Number([1,2,3]) //NaN
Number(true) //1
Number(false) //0
Number('123') //123
Number('abc') //NaN
Number('') //0
Number(Symbol(123)) //Uncaught TypeError: Cannot convert a Symbol value to a number
按位逻辑操作符
按位逻辑操作符(按位与 &
, 按位或 |
,按位异或 ^
,按位非 ~
)的规则:操作数被转换成 32
位二进制整数,超过 32
位的数字会被丢弃。第一个操作数的每个比特位与第二个操作数的相应比特位匹配(匹配规则见下方无序列表):第一位对应第一位,第二位对应第二位,以此类推。位运算符应用到每对比特位,结果是新的比特值。
位匹配规则:
– 按位与 &
:对每对比特位执行与(AND
)操作。只有 a
和 b
都是 1
时,a AND b
才是 1
。将任一数值 x
与 0
执行按位与操作,其结果都为 0
。将任一数值 x
与 -1
执行按位与操作,其结果都为 x
。
– 按位或 |
:对每一对比特位执行或(OR
)操作。如果 a
或 b
为 1
,则 a OR b
结果为 1
。将任一数值 x
与 0
进行按位或操作,其结果都是 x
。将任一数值 x
与 -1
进行按位或操作,其结果都为 -1
。
– 按位异或 ^
: 对每一对比特位执行异或(XOR
)操作。当 a
和 b
不相同时,a XOR b
的结果为 1
。将任一数值 x
与 0
进行异或操作,其结果为 x
。将任一数值 x
与 -1
进行异或操作,其结果为 ~x
。
– 对每一个比特位执行非(NOT
)操作。NOT a
结果为 a
的反码。对任一数值 x
进行按位非操作的结果为 -(x + 1)
。比如 ~str.indexOf(searchFor)
可以判断字符 searchFor
是否在 str
中出现,当 indexOf
返回 -1
时,~str.indexOf(searchFor)
的结果为 0
。
这里需要注意,所有的按位操作符都是用
32
位有符号二进制补码进行计算的,所以上面的位匹配规则中用-1
来说明,在补码中-1
就是全为1
。~x
表示x
取反。NaN
作为操作数的表现和0
相同,比如({}) & -1
的结果为0
。
按位移动操作符
按位移动操作符(按位左移 <<
,有符号按位右移 >>
,无符号按位右移 >>>
)有两个操作数:第一个是要被移动的数字,而第二个是要移动的长度。移动的方向根据操作符的不同而不同。
按位移动会先将操作数转换为大端字节序顺序(big-endian order
)的 32
位整数,并返回与左操作数相同类型的结果。右操作数应小于 32
位,否则只有最低 5
个字节会被使用。
Big-Endian
:高位字节排放在内存的低地址端,低位字节排放在内存的高地址端,又称为”高位编址”,是最直观的字节序。
- 按位左移
<<
:该操作符会将第一个操作数向左移动指定的位数。向左被移出的位被丢弃,右侧用0
补充。9 << 2
结果为36
,即1001
变为100100
。 - 有符号按位右移
>>
:该操作符会将第一个操作数向右移动指定的位数。向右被移出的位被丢弃,拷贝最左侧的位以填充左侧。由于新的最左侧的位总是和以前相同,符号位没有被改变。所以被称作“符号传播”。9>>2
结果为2
,即1001
变为0010
;-9>>2
结果为-3
,即11111111111111111111111111110111
变为11111111111111111111111111111101
。 - 无符号按位右移
>>>
:该操作符会将第一个操作数向右移动指定的位数。向右被移出的位被丢弃,左侧用0
填充。因为符号位变成了0
,所以结果总是非负的。对于非负数,有符号右移和无符号右移总是返回相同的结果。(即便右移0
个比特,结果也是非负的,可以理解为将补码直接当做正数,比如--1
的补码是32
个1
,那么-1 >>>0
的结果就是4294967295
)
应用
上面介绍了 JavaScript
中的位运算概念,本节介绍一些比较常见的应用场景。
掩码 bitmask
所谓掩码其实就是一串二进制数,我们通过 与/或 的操作来读取标志位。主要利用的特性就是任何数与 0
进行与操作都是 0
,任何数与 1
进行或操作都是 1
。
比如我们常见的子网掩码就是一种应用,为了区分我们 IP
地址中的网络号与主机号,用子网掩码来做 与 操作即可。我们的 IP
的地址是 4
字节,子网掩码也是 4
字节,当我们的子网掩码是 255.255.255.0
的时候,其实就是 11111111.11111111.11111111.00000000
,当我们的 IP
地址和子网掩码进行与操作之后,只会留下前面 24
位,这 24
位也就是我们的主机号。也就是说,子网掩码就是将网络号对应的位全部设为 1
,而主机号的位设为 0
,可以很方便的知道我们的网络号和主机号。
在 JavaScript
中也有应用,比如 MouseEvent.buttons
就是用的掩码的形式来分辨当前按下了哪些鼠标上的键,用六位二进制数作为标志位,
- 0 : 没有按键或者是没有初始化
- 1 : 鼠标左键
- 2 : 鼠标右键
- 4 : 鼠标滚轮或者是中键
- 8 : 第四按键 (通常是“浏览器后退”按键)
- 16 : 第五按键 (通常是“浏览器前进”)
我们可以利用掩码来进行一些状态的保存和判断,用法主要是
- 用或运算设置标志位,掩码中为
1
的位可以设置对应的位,比如0011
就可以将最后两位都设置为1
。 - 用与运算清除标志位,利用掩码中的
0
将目标位置进行重置。比如0000
可以将目标的四位全部置为0
。 - 用与运算获得目标位置的状态,比如
0010
与目标进行与运算可以获得目标第三位的状态。 - 用异或运算切换标志位状态,掩码中的
1
能够让目标位置的状态切换。
我们可以用左移运算符进行掩码的自动化创建,无符号右移将掩码转化为布尔值:
//布尔值转掩码
function createMask () {
var nMask = 0, nFlag = 0, nLen = arguments.length > 32 ? 32 : arguments.length;
for (nFlag; nFlag < nLen; nMask |= arguments[nFlag] << nFlag++);
return nMask;
}
var mask1 = createMask(true, true, false, true); // 11, 0b1011
var mask2 = createMask(false, false, true); // 4, 0b0100
var mask3 = createMask(true); // 1, 0b0001
//掩码转布尔值
function arrayFromMask (nMask) {
// nMask 必须介于 -2147483648 和 2147483647 之间,去除符号位还有31位
if (nMask > 0x7fffffff || nMask < -0x80000000) {
throw new TypeError("arrayFromMask - out of range");
}
for (var nShifted = nMask, aFromMask = []; nShifted;
aFromMask.push(Boolean(nShifted & 1)), nShifted >>>= 1);
return aFromMask;
}
var array1 = arrayFromMask(11); //[true, true, false, true]
var array2 = arrayFromMask(4); //[false, false, true]
var array3 = arrayFromMask(1); //[true]
乘除
我们的右移相当于除2,左移相当于乘2。
let a = -10;
a >> 1; // -5
a << 1; // -20
交换两个数的值
利用异或的特点,我们可以不创建第三个变量进行两个变量值的交换,
let a = 100, b = -100;
a ^ = b; //a = a ^ b
b ^ = a; //b = b ^ b ^ a -> a
a ^ = b; // a = a ^ a ^ b -> b
这主要利用的是异或的结合律和交换律,即 a ^ b
和 b ^ a
是相等的;以及异或自身的结果为 0
,任何值和 0
进行异或结果都为自身。
判断奇偶
二进制数只有最后一位是 0
就是偶数,最后一位是 1
就是奇数,我们可以利用这一点进行判断,
if ( 0 === (a & 1)) {
console.log('an even number')
} else {
console.log('an odd number')
}
变更符号
对于二进制补码,有 X + ~X + 1 = 0
,变形可得 -X = ~X + 1
,所以一个数的相反数就是按位取反再加一。
let a = 10;
console.log(~a + 1); //-10
利用这一特性我们也可以求绝对值,只要先判断值的正负即可。利用 a >> 31
即可判断正负,正数右移 31
位后结果为 0
,负数右移 31
位后结果为 -1
,如果是负数则求其相反数。求绝对值还有一个更简化一点的办法:
let a = -10
let i = a >> 31;
console.log((a^i) - i); //10
主要就是利用任何数与 -1
进行异或运算就是取反。
高低位交换
比如一个 16位
无符号整数,我们可以利用位运算进行高八位第八位的交换。a << 8 | a >> 8
求最小的2的整数次幂约数
给定一个数,想求其最小的整数次幂约数可以用 a & -a
。求最小的 2
的整数次幂约数其实就是找其二进制表示中从右往左数第一个 1
。利用 -a = ~a + 1
可以找到这个 1
的位置。
计算二进制数中 1
的个数
let count = 0;
let a = 100;
while(a){
a = a & (a - 1);
count++;
}
console.log(count) //3
console.log(100. toString(2)) //1100100
小数转整数
由于按位操作符的计算都会先将操作数转为 32
位有符号二进制数,我们可以利用这个来将小数转为整数。比如我们需要随机数的时候 Math.random() * 16 | 0
就能够直接得到整数。