阅读 2961

【译】JavaScript中按位操作符的有趣应用

原文标题:Interesting use cases for JavaScript bitwise operators

原文地址:blog.logrocket.com/interesting…

本文首发于公众号:符合预期的CoyPan

JavaScript提供了几种运算符,可以对一些简单的值进行基本操作,比如算术操作、赋值操作、逻辑操作、按位操作等。

我们经常可以看到混合了赋值操作,算术操作和逻辑操作的JavaScript代码。但是,按位操作的代码就不是那么常见了。

JavaScript的按位操作符

  1. ~按位非
  2. &按位与
  3. |按位或
  4. ^按位异或
  5. <<左移
  6. >>有符号右移
  7. >>>无符号右移

在本文中,我们将过一遍所有的按位操作符并且试着理解他们是怎么工作的。同时,我们会编写简单的JavaScript的代码,来看一看一些有趣的按位操作符运用。这需要我们了解一下javascript位操作符如何将其操作数表示为有符号的32位整数。让我们开始吧。

按位非(~)

~运算符是一元运算符;因此,它只需要一个操作数。~运算符对其操作数的每一位执行NOT操作。非运算的结果称为补码。整数的补码是通过将整数的每一位倒转而形成的。

对于给定的整数(例如170),可以使用~运算符计算补码,如下所示:

// 170 => 00000000000000000000000010101010
// --------------------------------------
//  ~ 00000000000000000000000010101010
// --------------------------------------
//  = 11111111111111111111111101010101
// --------------------------------------
//  = -171 (decimal)

console.log(~170); // -171
复制代码

javascript按位运算符将其操作数转换为二进制补码格式的32位有符号整数。因此,当对整数使用~运算符时,得到的值是整数的补码。整数A的补码的结果为 - (A+1) 。

~170 => -(170 + 1) => -171
复制代码

下面是一些需要注意的关于32位有符号整数的要点,这些整数由javascript位运算符使用:

  • 最有意义(最左边)的位称为符号位。正整数的符号位总是0,负整数的符号位总是1。
  • 除符号位之外的其余31位用于表示整数。因此,可以表示的最大32位整数是(2^32-1),它是2147483647,而最小整数是(2^31),它是-2147483648。
  • 对于不在32位有符号整数范围内的整数,最有效位将被丢弃,直到整数在该范围内。

以下是一些重要数字的32位序列表示:

0 => 00000000000000000000000000000000
-1 => 11111111111111111111111111111111
2147483647 => 01111111111111111111111111111111
-2147483648 => 10000000000000000000000000000000
复制代码

从上面的描述可以很容易得出:

				  ~0 => -1
         ~-1 => 0
 ~2147483647 => -2147483648
~-2147483648 => 2147483647
复制代码

找到索引

大多数JavaScript内置对象(如数组和字符串)都有一些有用的方法,可用于检查数组中是否存在项或字符串中是否存在子字符串。以下是一些方法:

  • Array.indexOf()
  • Array.lastIndexOf()
  • Array.findIndex()
  • String.indexOf()
  • String.lastIndexOf()
  • String.search()

这些方法都返回某一项或子字符串的从零开始的索引(如果找到);否则,它们返回-1。例如:

const numbers = [1, 3, 5, 7, 9];

console.log(numbers.indexOf(5)); // 2
console.log(numbers.indexOf(8)); // -1
复制代码

如果我们对么某一项或者子字符串的索引位置不感兴趣,我们可以选择使用布尔值。当未找到的项或者子字符串时,返回-1,我们可以认为是false,返回其他的值都是true。

function foundIndex (index) {
  return Boolean(~index);
}
复制代码

在上面的代码片段中,~运算符在-1上使用时的值为0。使用boolean()将值强制转换为boolean,返回false。对于其他每个索引值,返回true。因此,以前的代码段可以修改如下:

const numbers = [1, 3, 5, 7, 9];

console.log(foundIndex(numbers.indexOf(5))); // true
console.log(foundIndex(numbers.indexOf(8))); // false
复制代码

按位与(&)

& 操作符对其操作数的每一对对应位执行一个和运算。& 操作符仅当两个位都为1时返回1;否则返回0。因此,与运算的结果等于将每一对对应的位相乘。

下面是与操作的可能值:

(0 & 0) === 0     // 0 x 0 = 0
(0 & 1) === 0     // 0 x 1 = 0
(1 & 0) === 0     // 1 x 0 = 0
(1 & 1) === 1     // 1 x 1 = 1
复制代码

'关闭'某些位

&操作符通常用于位屏蔽应用,以确保为给定的位序列关闭某些位。这是基于这样一个事实,即对于任何位A:

  • (A & 0 = 0) — 和0进行与运算,位总是会变成0。
  • (A & 1 = A) — 和1进行与运算,位总是保持不变。

举个例子,假设我们有一个8位的整数,我们希望确保前面的4位被关闭(置为0)。我们可以用&操作符来实现:

  • 首先,创建一个位掩码,其效果是关闭8位整数的前4位。该位掩码将为0B111110000。请注意,位掩码的前4位设置为0,而其他每一位设置为1。
  • 接下来,使用8位整数和创建的位掩码进行 &操作。
const mask = 0b11110000;

// 222 => 11011110

// (222 & mask)
// ------------
// 11011110
// & 11110000
// ------------
// = 11010000
// ------------
// = 208 (decimal)

console.log(222 & mask); // 208
复制代码

检查设定位

&操作符还有一些其他有用的位屏蔽应用。一个这样的应用是确定给定的位序列是否设置了一个或多个位。例如,假设我们要检查是否为给定的十进制数设置了第五位。以下是我们如何使用&运算符来执行此操作:

  • 首先,创建一个位掩码,用于检查目标位(在本例中为第五位)是否设置为1。位掩码上的每个位都设置为0,但目标位置的位除外,目标位置的位设置为1。二进制数文字可用于轻松实现这一点:

    const mask = 0b10000;
    复制代码
  • 接下来,使用十进制数和位掩码作为操作数执行&操作,并将结果与位掩码进行比较。如果所有目标位都设置为十进制数,&操作的结果将等于位掩码。请注意,位掩码中的0位将有效地关闭十进制数中的相应位,因为a&0=0。

    // 34 => 100010
    // (34 & mask) => (100010 & 010000) = 000000
    console.log((34 & mask) === mask); // false
    
    // 50 => 110010
    // (50 & mask) => (110010 & 010000) = 010000
    console.log((50 & mask) === mask); // true
    复制代码

奇数或偶数

使用&运算符检查十进制数的设定位可以扩展到检查给定的十进制数是偶数还是奇数。为了实现这一点,使用1作为位掩码(以确定是否设置了第一位或最右边的位)。

对于整数,可以使用最低有效位(第一位或最右边的位)来确定数字是偶数还是奇数。如果启用最低有效位(设置为1),则数字为奇数;否则,数字为偶数。

function isOdd (int) {
  return (int & 1) === 1;
}

function isEven (int) {
  return (int & 1) === 0;
}

console.log(isOdd(34)); // false
console.log(isOdd(-63)); // true
console.log(isEven(-12)); // true
console.log(isEven(199)); // false
复制代码

有用的标识

在继续下一个运算符之前,这里有一些&操作符的有用标识(对于任何带符号的32位整数A):

(A & 0) === 0
(A & ~A) === 0
(A & A) === A
(A & -1) === A
复制代码

按位或(|)

运算符对其操作数的每对对应位执行“或”运算。运算符仅当两个位都为0时返回0;否则返回1。

对于一对位,这里是或操作的可能值:

(0 | 0) === 0
(0 | 1) === 1
(1 | 0) === 1
(1 | 1) === 1
复制代码

'打开'位

在位屏蔽应用中,可以使用运算符来确保位序列中的某些位被打开(设置为1)。这是基于这样一个事实:对于任何给定的位A:

  • (A | 0 = A) — 和0进行或运算,位总是会保持不变。
  • (A | 1 = 1) — 和1进行或运算,位总是为1。

例如,假设我们有一个8位整数,我们希望确保所有偶数位(第二、第四、第六、第八)都打开(设置为1)。| 运算符可用于实现以下目的:

  • 首先,创建一个位掩码,其效果是打开8位整数的每个偶数位。该位掩码将是0B101010。请注意,位掩码的偶数位设置为1,而其他位设置为0。
  • 接下来,使用8位整数和创建的位掩码执行或操作:
const mask = 0b10101010;

// 208 => 11010000

// (208 | mask)
// ------------
// 11010000
// | 10101010
// ------------
// = 11111010
// ------------
// = 250 (decimal)

console.log(208 | mask); // 250
复制代码

有用的标识

在继续下一个运算符之前,这里有一些 | 操作符的有用标识(对于任何带符号的32位整数A):

(A | 0) === A
(A | ~A) === -1
(A | A) === A
(A | -1) === -1
复制代码

按位异或(^)

^运算符对其操作数的每对对应位执行异或(异或)运算。如果两个位相同(0或1),则^运算符返回0;否则,它返回1。

对于一对位,下面是可能的值:

(0 ^ 0) === 0
(0 ^ 1) === 1
(1 ^ 0) === 1
(1 ^ 1) === 0
复制代码

切换位

在位屏蔽应用程序中,^ 运算符通常用于切换或翻转位序列中的某些位。这是基于这样一个事实:对于任何给定的位A:

  • 0进行异或运算,位总是会保持不变。

    (A ^ 0 = A)

  • 当与相应的1位配对时,该位总是被切换。

    (A ^ 1 = 1) — if A is 0 (A ^ 1 = 0) — if A is 1

例如,假设我们有一个8位整数,我们希望确保除了最低有效位(第一位)和最高有效位(第八位)之外,每个位都被切换。可以使用^运算符实现以下目的:

  • 首先,创建一个位掩码,其效果是切换8位整数的每个位,除了最低有效位和最高有效位。该位掩码将为0b0111110。请注意,要切换的位设置为1,而其他位设置为0。

  • 接下来,使用8位整数和创建的位掩码执行^操作:

    const mask = 0b01111110;
    
    // 208 => 11010000
    
    // (208 ^ mask)
    // ------------
    // 11010000
    // ^ 01111110
    // ------------
    // = 10101110
    // ------------
    // = 174 (decimal)
    
    console.log(208 ^ mask); // 174
    复制代码

有用的标识

在继续下一个运算符之前,以下是^操作的一些有用标识(对于任何有符号的32位整数A):

(A ^ 0) === A
(A ^ ~A) === -1
(A ^ A) === 0
(A ^ -1) === ~A
复制代码

从上面列出的标识中可以明显看出,-1上的xor操作等同于a上的按位非操作。因此,上面的foundIndex()函数也可以这样编写:

function foundIndex (index) {
  return Boolean(index ^ -1);
}
复制代码

左移(<<)

左移位(<<)运算符接受两个操作数。第一个操作数是整数,而第二个操作数是要向左移动的第一个操作数的位数。零(0)位从右边移入,而从左边移入的多余位被丢弃。

例如,考虑整数170。假设我们要向左移动三位。我们可以使用<<运算符,如下所示:

// 170 => 00000000000000000000000010101010

// 170 << 3
// --------------------------------------------
//    (000)00000000000000000000010101010(***)
// --------------------------------------------
//  = (***)00000000000000000000010101010(000)
// --------------------------------------------
//  = 00000000000000000000010101010000
// --------------------------------------------
//  = 1360 (decimal)

console.log(170 << 3); // 1360
复制代码

左移位位运算符(<<)可以使用以下javascript表达式定义:

(A << B) => A * (2 ** B) => A * Math.pow(2, B)
复制代码

因此,回顾前面的示例:

(170 << 3) => 170 * (2 ** 3) => 170 * 8 => 1360
复制代码

颜色转换:RGB到十六进制

左移位(<)运算符的一个非常有用的应用程序是将颜色从RGB表示转换为十六进制表示。

RGB颜色的每个组件的颜色值在0-255之间。简单地说,每个颜色值可以用8位完美地表示。

 0 => 0b00000000 (2进制) => 0x00 (16进制)
255 => 0b11111111 (2进制) => 0xff (16进制)
复制代码

因此,颜色本身可以完美地用24位来表示(红色、绿色和蓝色分量各8位)。从右边开始的前8位表示蓝色分量,接下来的8位表示绿色分量,之后的8位表示红色分量。

(binary) => 11111111 00100011 00010100

   (red) => 11111111 => ff => 255
 (green) => 00100011 => 23 => 35
  (blue) => 00010100 => 14 => 20

   (hex) => ff2314
复制代码

既然我们已经了解了如何将颜色表示为24位序列,那么让我们来看看如何从颜色的各个组件的值组成颜色的24位。假设我们有一个用RGB(255、35、20)表示的颜色。以下是我们如何组合这些位:

(red) => 255 => 00000000 00000000 00000000 11111111
(green) =>  35 => 00000000 00000000 00000000 00100011
 (blue) =>  20 => 00000000 00000000 00000000 00010100

// Rearrange the component bits and pad with zeroes as necessary
// Use the left shift operator

  (red << 16) => 00000000 11111111 00000000 00000000
 (green << 8) => 00000000 00000000 00100011 00000000
       (blue) => 00000000 00000000 00000000 00010100

// Combine the component bits together using the OR (|) operator
// ( red << 16 | green << 8 | blue )

      00000000 11111111 00000000 00000000
    | 00000000 00000000 00100011 00000000
    | 00000000 00000000 00000000 00010100
// -----------------------------------------
      00000000 11111111 00100011 00010100
// -----------------------------------------
复制代码

既然过程非常清楚,下面是一个简单的函数,它将颜色的RGB值作为输入数组,并基于上述过程返回颜色的相应十六进制表示:

function rgbToHex ([red = 0, green = 0, blue = 0] = []) {
  return `#${(red << 16 | green << 8 | blue).toString(16)}`;
}
复制代码

有符号右移(>>)

有符号右移(>>)运算符的符号接受两个操作数。第一个操作数是整数,而第二个操作数是要右移的第一个操作数的位数。

已移到右边的多余位将被丢弃,而符号位(最左边的位)的副本将从左边移入。所以,整数的符号位会一直保留。所以这种运算叫做有符号右移。

例如,考虑整数170和-170。假设我们想把三位移到右边。我们可以使用>>运算符,如下所示:

//  170 => 00000000000000000000000010101010
// -170 => 11111111111111111111111101010110

// 170 >> 3
// --------------------------------------------
//    (***)00000000000000000000000010101(010)
// --------------------------------------------
//  = (000)00000000000000000000000010101(***)
// --------------------------------------------
//  = 00000000000000000000000000010101
// --------------------------------------------
//  = 21 (decimal)

// -170 >> 3
// --------------------------------------------
//    (***)11111111111111111111111101010(110)
// --------------------------------------------
//  = (111)11111111111111111111111101010(***)
// --------------------------------------------
//  = 11111111111111111111111111101010
// --------------------------------------------
//  = -22 (decimal)

console.log(170 >> 3); // 21
console.log(-170 >> 3); // -22
复制代码

通过以下javascript表达式可以描述有符号右移:

(A >> B) => Math.floor(A / (2 ** B)) => Math.floor(A / Math.pow(2, B))
复制代码

因此,之前的那个例子可以如下表示:

(170 >> 3) => Math.floor(170 / (2 ** 3)) => Math.floor(170 / 8) => 21
(-170 >> 3) => Math.floor(-170 / (2 ** 3)) => Math.floor(-170 / 8) => -22
复制代码

颜色提取

有符号右移(>>)运算符的一个非常好的应用是从颜色中提取RGB颜色值。当颜色以RGB表示时,很容易区分红色、绿色和蓝色颜色分量值。但是,对于以十六进制表示的颜色,这将花费更多的精力。

在上一节中,我们看到了从颜色的各个组成部分(红色、绿色和蓝色)的位组成颜色的过程。如果我们反向执行这个过程,我们将能够提取颜色的各个组成部分的值。让我们试一试。

假设我们有一个用十六进制表示法ff2314表示的颜色。下面是颜色的有符号32位表示:

(color) => ff2314 (hexadecimal) => 11111111 00100011 00010100 (binary)

// 32-bit representation of color
00000000 11111111 00100011 00010100
复制代码

为了获得单个部分,我们将根据需要将颜色位按8的倍数右移,直到从右边得到目标组件位作为前8位。由于颜色的32位中的符号标志位是0,因此我们可以安全地使用符号传播右移位(>>)运算符。

color => 00000000 11111111 00100011 00010100

// Right shift the color bits by multiples of 8
// Until the target component bits are the first 8 bits from the right

  red => color >> 16
      => 00000000 11111111 00100011 00010100 >> 16
      => 00000000 00000000 00000000 11111111

green => color >> 8
      => 00000000 11111111 00100011 00010100 >> 8
      => 00000000 00000000 11111111 00100011

 blue => color >> 0 => color
      => 00000000 11111111 00100011 00010100
复制代码

现在我们将目标颜色位作为右前8位,我们需要一种方法来屏蔽除前8位之外的所有其他位。这使我们回到和(&)运算符。请记住,&运算符可用于确保关闭某些位。

让我们从创建所需的位掩码开始。就像这样:

mask => 00000000 00000000 00000000 11111111
     => 0b11111111 (binary)
     => 0xff (hexadecimal)
复制代码

准备好位掩码后,我们可以对上一次右移操作的每个结果执行与(&)操作,使用位掩码提取目标颜色。

red => color >> 16 & 0xff
      =>   00000000 00000000 00000000 11111111
      => & 00000000 00000000 00000000 11111111
      => = 00000000 00000000 00000000 11111111
      =>   255 (decimal)

green => color >> 8 & 0xff
      =>   00000000 00000000 11111111 00100011
      => & 00000000 00000000 00000000 11111111
      => = 00000000 00000000 00000000 00100011
      =>   35 (decimal)

 blue => color & 0xff
      =>   00000000 11111111 00100011 00010100
      => & 00000000 00000000 00000000 11111111
      => = 00000000 00000000 00000000 00010100
      =>   20 (decimal)
复制代码

基于上述过程,这里有一个简单的函数,它以十六进制颜色字符串(带有六个十六进制数字)作为输入,并返回相应的RGB颜色分量值数组。

function hexToRgb (hex) {
  hex = hex.replace(/^#?([0-9a-f]{6})$/i, '$1');
  hex = Number(`0x${hex}`);

  return [
    hex >> 16 & 0xff, // red
    hex >> 8 & 0xff,  // green
    hex & 0xff        // blue
  ];
}
复制代码

无符号右移(>>>)

无符号右移位(>>>)运算符的行为非常类似于符号传播右移位(>>)运算符。然而,关键区别在于从左边移入的位。

顾名思义,0位总是从左边移入。因此,>>运算符始终返回无符号32位整数,因为结果整数的符号位始终为0。对于正整数,>>和>>>都将始终返回相同的结果。

例如,考虑整数170和-170。假设我们要将3位移到右边,我们可以使用>>>操作符,如下所示:

//  170 => 00000000000000000000000010101010
// -170 => 11111111111111111111111101010110

// 170 >>> 3
// --------------------------------------------
//    (***)00000000000000000000000010101(010)
// --------------------------------------------
//  = (000)00000000000000000000000010101(***)
// --------------------------------------------
//  = 00000000000000000000000000010101
// --------------------------------------------
//  = 21 (decimal)

// -170 >>> 3
// --------------------------------------------
//    (***)11111111111111111111111101010(110)
// --------------------------------------------
//  = (000)11111111111111111111111101010(***)
// --------------------------------------------
//  = 00011111111111111111111111101010
// --------------------------------------------
//  = 536870890 (decimal)

console.log(170 >>> 3); // 21
console.log(-170 >>> 3); // 536870890
复制代码

配置标志

在总结本教程之前,让我们考虑另一个非常常见的位操作符和位屏蔽应用:配置标志。

假设我们有一个函数,它接受几个布尔选项,这些选项可以用来控制函数的运行方式或返回的值的类型。创建此函数的一种可能方法是将所有选项作为参数传递给该函数,可能使用一些默认值,例如:

function doSomething (optA = true, optB = true, optC = false, optD = true, ...) {
  // something happens here...
}
复制代码

当然,这不太方便。在以下两种情况下,这种方法开始变得相当有问题:

  • 假设我们有10个以上的布尔选项。我们不能用这么多参数定义函数。
  • 假设我们只想为第五个和第九个选项指定一个不同的值,并让其他选项保留默认值。我们需要调用函数,将默认值作为所有其他选项的参数传递,同时为第五个和第九个选项传递所需的值。

用前面的方法解决问题的一种方法是为配置选项使用一个对象,如下所示:

const defaultOptions = {
  optA: true,
  optB: true,
  optC: false,
  optD: true,
  ...
};

function doSomething (options = defaultOptions) {
  // something happens here...
}
复制代码

这种方法非常优雅,您很可能已经看到它被使用了,甚至自己在某个地方使用过。然而,使用这种方法时,options参数将始终是一个对象,对于配置选项来说,这可以被认为是多余的。

如果所有选项都采用布尔值,则可以使用整数而不是对象来表示选项。在这种情况下,整数的某些位将映射到指定的选项。如果某个位被打开(设置为1),则指定选项的值为“真”;否则为“假”。

我们可以用一个简单的例子来演示这种方法。假设我们有一个函数,它规范化包含数字的数组列表中的项,并返回规范化的数组。返回的数组可以由三个选项控制,即:

  • fraction:将数组中的每个项除以数组中的最大项

  • unique:从数组中删除重复项

  • sorted:将数组中的项从最低到最高排序

我们可以使用一个3位整数来表示这些选项,每个位都映射到一个选项。以下代码段显示选项标志:

const LIST_FRACTION = 0x1; // (001)
const LIST_UNIQUE = 0x2;   // (010)
const LIST_SORTED = 0x4;   // (100)
复制代码

要激活一个或多个选项,可以根据需要使用运算符组合相应的标志。例如,我们可以创建一个标志来激活所有选项,如下所示:

const LIST_ALL = LIST_FRACTION | LIST_UNIQUE | LIST_SORTED; // (111)
复制代码

同样,假设我们只希望默认情况下激活fraction和sorted选项。我们可以再次使用运算符,如下所示:

const LIST_DEFAULT = LIST_FRACTION | LIST_SORTED; // (101)
复制代码

虽然只使用三个选项看起来并不糟糕,但当有这么多选项时,它往往会变得非常混乱,并且默认情况下需要激活其中的许多选项。在这种情况下,更好的方法是使用^运算符停用不需要的选项:

const LIST_DEFAULT = LIST_ALL ^ LIST_UNIQUE; // (101)
复制代码

这里,我们有一个列表“所有”标志,可以激活所有选项。然后,我们使用^运算符停用唯一选项,并根据需要保留其他选项。

现在我们已经准备好了选项标志,可以继续定义normalizelist()函数:

function normalizeList (list, flag = LIST_DEFAULT) {
  if (flag & LIST_FRACTION) {
    const max = Math.max(...list);
    list = list.map(value => Number((value / max).toFixed(2)));
  }
  if (flag & LIST_UNIQUE) {
    list = [...new Set(list)];
  }
  if (flag & LIST_SORTED) {
    list = list.sort((a, b) => a - b);
  }
  return list;
}
复制代码

为了检查某个选项是否被激活,我们使用&运算符来检查该选项的相应位是否被打开(设置为1)。&操作是通过传递给函数的flag参数和选项的对应标志来执行的,如下面的代码段所示:

// Checking if the unique option is activated
// (flag & LIST_UNIQUE) === LIST_UNIQUE (activated)
// (flag & LIST_UNIQUE) === 0 (deactivated)

flag & LIST_UNIQUE
复制代码

总结

嘿,我真的很高兴你能读完这篇文章,尽管读了很长时间,但我强烈希望你在读的时候学到一两件事。谢谢你的时间。

正如我们在本文中所看到的,虽然使用得很谨慎,但javascript的位操作符有一些非常有趣的用例。我强烈希望您在阅读本文的过程中获得的见解从现在起用在你的日常开发中。

文章分类
前端
文章标签