Clloz's Blog

Back

JavaScript 中的按位操作符 Blur image

前言#

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
javascript

按位逻辑操作符#

按位逻辑操作符(按位与 &, 按位或 |,按位异或 ^,按位非 ~)的规则:操作数被转换成 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]
javascript

乘除#

我们的右移相当于除2,左移相当于乘2

let a = -10
a >> 1 // -5
a << 1 // -20
javascript

交换两个数的值#

利用异或的特点,我们可以不创建第三个变量进行两个变量值的交换,

let a = 100, b = -100;
a ^ = b; //a = a ^ b
b ^ = a; //b = b ^ b ^ a -> a
a ^ = b; // a = a ^ a ^ b -> b
javascript

这主要利用的是异或的结合律和交换律,即 a ^ bb ^ a 是相等的;以及异或自身的结果为 0,任何值和 0 进行异或结果都为自身。

判断奇偶#

二进制数只有最后一位是 0 就是偶数,最后一位是 1 就是奇数,我们可以利用这一点进行判断,

if (0 === (a & 1)) {
  console.log('an even number')
} else {
  console.log('an odd number')
}
javascript

变更符号#

对于二进制补码,有 X + ~X + 1 = 0,变形可得 -X = ~X + 1,所以一个数的相反数就是按位取反再加一。

let a = 10
console.log(~a + 1) //-10
javascript

利用这一特性我们也可以求绝对值,只要先判断值的正负即可。利用 a >> 31 即可判断正负,正数右移 31 位后结果为 0,负数右移 31 位后结果为 -1,如果是负数则求其相反数。求绝对值还有一个更简化一点的办法:

let a = -10
let i = a >> 31
console.log((a ^ i) - i) //10
javascript

主要就是利用任何数与 -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
javascript

小数转整数#

由于按位操作符的计算都会先将操作数转为 32 位有符号二进制数,我们可以利用这个来将小数转为整数。比如我们需要随机数的时候 Math.random() * 16 | 0 就能够直接得到整数。

参考文章#

  1. 位运算有什么奇技淫巧? - 力扣(LeetCode)的回答 - 知乎
  2. 按位操作符 - MDN
JavaScript 中的按位操作符
https://clloz.com/blog/bitwise-operator
Author Clloz
Published at October 4, 2020
Comment seems to stuck. Try to refresh?✨