【譯】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的位操做符有一些很是有趣的用例。我強烈但願您在閱讀本文的過程當中得到的看法從如今起用在你的平常開發中。

相關文章
相關標籤/搜索