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

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

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

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

JavaScript提供了几种运算符,能够对一些简单的值进行基本操做,好比算术操做、赋值操做、逻辑操做、按位操做等。安全

咱们常常能够看到混合了赋值操做,算术操做和逻辑操做的JavaScript代码。可是,按位操做的代码就不是那么常见了。app

JavaScript的按位操做符

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

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

按位非(~)

~运算符是一元运算符;所以,它只须要一个操做数。~运算符对其操做数的每一位执行NOT操做。非运算的结果称为补码。整数的补码是经过将整数的每一位倒转而造成的。ui

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

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

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

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

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

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

  • 最有意义(最左边)的位称为符号位。正整数的符号位老是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位整数,咱们但愿确保全部偶数位(第2、第4、第6、第八)都打开(设置为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(25五、3五、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的位操做符有一些很是有趣的用例。我强烈但愿您在阅读本文的过程当中得到的看法从如今起用在你的平常开发中。

相关文章
相关标签/搜索