漲姿式!原來JavaScript運算符還能夠這麼玩

漲姿式!原來JavaScript運算符還能夠這麼玩


image.png




做者 | Glad Chinda譯者 | 王強編輯 | Yoniejavascript

JavaScript 提供了幾種運算符,能夠用來對簡單的值執行一些基本操做,如算術運算、賦值、邏輯運算、按位運算等。前端

JavaScript 代碼中賦值、算術和邏輯三種運算符常常會混合使用,但按位運算符就用得沒那麼多了。java

JavaScript 按位運算符
  1. ~ - 按位非(NOT)編程

  2. & - 按位與(AND)數組

  3. |  - 按位或(OR)安全

  4. ^  - 按位異或(XOR)前端工程師

  5. <<  - 左移框架

  6. >>  -  符號傳播(有符號)右移ide

  7.  >>>  - 零填充(無符號)右移函數

這篇教程會逐一介紹 JavaScript 的按位運算符,並總結它們的運算機制。咱們還會研究一些實際的 JavaScript 程序案例。這裏還要了解一下 JavaScript 按位運算符是怎樣將其操做數表示爲有符號的 32 位整數的。

按位非(~)

~ 運算符是一個一元運算符,只須要一個操做數。~ 運算符對其操做數的每一比特位都執行一次非(NOT)運算。非運算的結果稱爲反碼。一個整數的反碼就是整數的每一個比特位都反轉的結果。

對於給定的整數,好比 170,可使用~ 運算符計算反碼,以下所示:

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

console.log(~170); // -171

JavaScript 按位運算符將其操做數轉換爲補碼格式的 32 位有符號整數。所以對整數使用~ 運算符時,結果值是整數的補碼。整數 A 的補碼由 -(A + 1) 給出。

~170 => -(170 + 1) => -171
關於 JavaScript 按位運算符使用的 32 位有符號整數有幾點注意事項:
  • 最高位(最左邊)稱爲符號位。正整數的符號位始終爲 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

若是咱們對找到的項或子字符串的索引不感興趣,則可使用布爾值,這樣未找到項目或子字符串時返回 false 來代替 -1,而找到其餘值時返回 true。代碼以下:

function foundIndex (index) {
 return Boolean(~index);
}

在上面的代碼中,~ 運算符在 -1 上使用時計算結果爲 0,這是一個虛值。所以使用 Boolean() 將虛值轉換爲布爾值,返回 false。對於其餘索引值則返回 true。所以上面的代碼段能夠修改以下:

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

console.log(foundIndex(numbers.indexOf(5))); // true
console.log(foundIndex(numbers.indexOf(8))); // false
按位與(&)

& 運算符對其操做數的每對比特位執行與(AND)運算。僅當兩個位都爲 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 時,該位關閉。

  • (A&1 = A) - 對應的位是 1 時,該位不變。

例如咱們有一個 8 位整數,咱們但願關閉右數前 4 位(設置爲 0),就能夠用 & 運算符實現此目的:

  • 首先建立一個位掩碼,其效果是關閉 8 位整數的右數前 4 位。這個位掩碼爲 0b11110000。注意位掩碼的右數前 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;
  • 接下來,使用十進制數和位掩碼做爲操做數執行 & 操做,並將結果與位掩碼對比。若是全部目標位都設置爲這個十進制數,則 & 操做的結果將等於位掩碼。注意,因爲 A&0 = 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
按位或(|)

| 運算符對其操做數的每對比特位執行或(OR)運算。只有當兩個位都爲 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 時,該位打開。

例如咱們有一個 8 位整數,咱們但願全部偶數位(第2、4、6、八位)都打開(設置爲 1)。因而能夠用| 運算符實現此目的:
  • 首先建立一個位掩碼,其效果是打開 8 位整數的每一個偶數位。這個位掩碼是 0b10101010。注意位掩碼的偶數位設置爲 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
按位異或(^)

^ 運算符對其操做數的每對比特位執行異或(XOR)運算。若是兩個位相同(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) - 若是 A 爲 0(A ^ 1 = 0) - 若是 A 是 1

例如咱們有一個 8 位整數,咱們但願每一個位都被翻轉,最低位和最高位除外。這裏能夠用 ^ 運算符實現,以下所示:
  • 首先建立一個位掩碼,其效果是翻轉 8 位整數除最低位和最高位以外的每一個位。這個位掩碼是 0b01111110。注意,要翻轉的位設置爲 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

如上所示,對 A 和 -1 執行異或運算至關於對 A 執行非運算。所以以前的 foundIndex() 函數也能夠這樣寫:

function foundIndex (index) {
 return Boolean(index ^ -1);
}
左移(<<)< span="">

左移(<<)運算符須要兩個操做數。第一個操做數是整數,而第二個操做數是第一個操做數要向左移位的位數。右面空出來的位用 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 (binary) => 0x00 (hexadecimal)
255 => 0b11111111 (binary) => 0xff (hexadecimal)

所以顏色能夠用 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

// 從新排列位,補上必要的 0
// 使用左移運算符

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

// 使用或 (|) 運算符組合起來
// ( 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
配置標誌

最後討論另外一個很是常見的按位運算符和位掩碼應用:配置標誌(flag)。

假設咱們有一個函數,其接受一些 boolean 選項,這些選項可用於控制函數的運行方式或返回的值類型。一種方法是將全部選項做爲參數傳遞給函數,可能還有一些默認值,以下所示:

function doSomething (optA = true, optB = true, optC = false, optD = true, ...) {
 // 這個函數作一些事情……
}

固然這樣不太方便。下面兩種狀況下這種方法會變得很是複雜:

  • 假若有超過 10 個布爾選項。咱們沒法使用那麼多參數定義咱們的函數。

  • 假如咱們只想爲第五個和第九個選項指定新的值,其餘選項則保留其默認值。此時咱們須要調用該函數,對其餘選項的參數傳遞默認值,對第五個和第九個選項則傳遞指定的值。

解決這個問題的一種方法是使用一個對象做爲配置選項,以下所示:

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

function doSomething (options = defaultOptions) {
 // 作一些事情……
}

這種方法很是優雅,最爲常見。但使用這種方法時 options 參數始終是一個對象,若是隻是用來配置選項的話未免殺雞用牛刀了。

若是全部選項都採用布爾值,咱們可使用整數而不是對象來表示選項。在這種狀況下,整數的特定位將映射到指定的選項。若是某個位被打開(設置爲 1),則指定選項的值爲 true;不然爲 false。

舉一個簡單的例子。假設咱們有一個函數來規範化包含數字的數組列表的項目並返回規範化數組。返回的數組能夠經過三個選項控制,即:

  • 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)

這裏咱們用 LIST_ALL 標誌來激活全部選項。而後咱們使用 ^ 運算符停用某些選項,其餘選項則保持激活狀態。

如今咱們已經準備好了選項標誌,就能夠繼續定義 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 按位運算符用得很少,但它有一些很是有趣的用例。但願你們能在實際編程工做中用到本文學到的內容。

編程快樂!

英文原文: https://blog.logrocket.com/interesting-use-cases-for-javascript-bitwise-operators/

 活動推薦

前端團隊能力如何發展,才能在研發組織中體現更多價值?新框架千千萬,前端 leader 要如何爲組織作選擇?極客時間提供前端團隊基礎技能培養和知識拓展所需系列課程,幫助前端工程師強化崗位知識結構,提高問題解決能力和創新能力。團隊中不一樣技能水平的開發者能在極客時間平臺上共同窗習,根據我的基礎選擇課程;leader 隨時掌握團隊學習進度,激勵團隊成長。詳細瞭解團隊學習模式,請掃描圖上二維碼,或點擊「閱讀原文」。

相關文章
相關標籤/搜索