JavaScript 中的按位操作符

Clloz · · 99次浏览 ·

前言

JavaScript 提供了多种按位操作符,由于不是很了解,我使用频率很低。不过经常看到别人的代码中利用按位操作符简化代码,在一些场景下能够更有效率。本文记录学习按位操作符的笔记。

按位操作符

JavaScript 中的按位操作符见下表:

运算符 用法 描述
按位与( AND a & b 对于每一个比特位,只有两个操作数相应的比特位都是 1 时,结果才为 1,否则为 0
按位或(OR a | b 对于每一个比特位,当两个操作数相应的比特位至少有一个 1 时,结果为 1,否则为 0
按位异或(XOR a ^ 对于每一个比特位,当两个操作数相应的比特位有且只有一个 1 时,结果为 1,否则为 0
按位非(NOT ~ a 反转操作数的比特位,即 0 变成 11 变成 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)操作。只有 ab 都是 1 时,a AND b 才是 1。将任一数值 x0 执行按位与操作,其结果都为 0。将任一数值 x-1 执行按位与操作,其结果都为 x
– 按位或 |:对每一对比特位执行或(OR)操作。如果 ab1,则 a OR b 结果为 1。将任一数值 x0 进行按位或操作,其结果都是 x。将任一数值 x-1 进行按位或操作,其结果都为 -1
– 按位异或 ^: 对每一对比特位执行异或(XOR)操作。当 ab 不相同时,a XOR b 的结果为 1。将任一数值 x0 进行异或操作,其结果为 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 的补码是 321,那么 -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 ^ bb ^ 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 就能够直接得到整数。

参考文章

  1. 位运算有什么奇技淫巧? – 力扣(LeetCode)的回答 – 知乎
  2. 按位操作符 – MDN

Clloz

Clloz

人生をやり直す

发表评论

电子邮件地址不会被公开。 必填项已用*标注

我不是机器人*

 

00:00/00:00