25

JavaScript 中的按位操作符

 3 years ago
source link: https://www.clloz.com/programming/front-end/js/2020/10/04/bitwise-operator/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

前言

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

按位操作符

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

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

参考文章

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

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK