原文標題:Interesting use cases for JavaScript bitwise operators原文地址:https://blog.logrocket.com/in...javascript
本文首發於公衆號:符合預期的CoyPanjava
JavaScript提供了幾種運算符,能夠對一些簡單的值進行基本操做,好比算術操做、賦值操做、邏輯操做、按位操做等。數組
咱們常常能夠看到混合了賦值操做,算術操做和邏輯操做的JavaScript代碼。可是,按位操做的代碼就不是那麼常見了。安全
在本文中,咱們將過一遍全部的按位操做符而且試着理解他們是怎麼工做的。同時,咱們會編寫簡單的JavaScript的代碼,來看一看一些有趣的按位操做符運用。這須要咱們瞭解一下javascript位操做符如何將其操做數表示爲有符號的32位整數。讓咱們開始吧。app
~運算符是一元運算符;所以,它只須要一個操做數。~運算符對其操做數的每一位執行NOT操做。非運算的結果稱爲補碼。整數的補碼是經過將整數的每一位倒轉而造成的。函數
對於給定的整數(例如170),可使用~運算符計算補碼,以下所示:rest
// 170 => 00000000000000000000000010101010 // -------------------------------------- // ~ 00000000000000000000000010101010 // -------------------------------------- // = 11111111111111111111111101010101 // -------------------------------------- // = -171 (decimal) console.log(~170); // -171
javascript按位運算符將其操做數轉換爲二進制補碼格式的32位有符號整數。所以,當對整數使用~運算符時,獲得的值是整數的補碼。整數A的補碼的結果爲 - (A+1) 。code
~170 => -(170 + 1) => -171
下面是一些須要注意的關於32位有符號整數的要點,這些整數由javascript位運算符使用:component
如下是一些重要數字的32位序列表示:orm
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:
0
進行與運算,位老是會變成0。1
進行與運算,位老是保持不變。舉個例子,假設咱們有一個8位的整數,咱們但願確保前面的4位被關閉(置爲0)。咱們能夠用&
操做符來實現:
&
操做。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:
0
進行或運算,位老是會保持不變。1
進行或運算,位老是爲1。例如,假設咱們有一個8位整數,咱們但願確保全部偶數位(第2、第4、第6、第八)都打開(設置爲1)。| 運算符可用於實現如下目的:
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)
(A ^ 1 = 1) — if A
is 0
(A ^ 1 = 0) — if A
is 1
例如,假設咱們有一個8位整數,咱們但願確保除了最低有效位(第一位)和最高有效位(第八位)以外,每一個位都被切換。可使用^運算符實現如下目的:
接下來,使用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顏色的每一個組件的顏色值在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... }
固然,這不太方便。在如下兩種狀況下,這種方法開始變得至關有問題:
用前面的方法解決問題的一種方法是爲配置選項使用一個對象,以下所示:
const defaultOptions = { optA: true, optB: true, optC: false, optD: true, ... }; function doSomething (options = defaultOptions) { // something happens here... }
這種方法很是優雅,您極可能已經看到它被使用了,甚至本身在某個地方使用過。然而,使用這種方法時,options參數將始終是一個對象,對於配置選項來講,這能夠被認爲是多餘的。
若是全部選項都採用布爾值,則可使用整數而不是對象來表示選項。在這種狀況下,整數的某些位將映射到指定的選項。若是某個位被打開(設置爲1),則指定選項的值爲「真」;不然爲「假」。
咱們能夠用一個簡單的例子來演示這種方法。假設咱們有一個函數,它規範化包含數字的數組列表中的項,並返回規範化的數組。返回的數組能夠由三個選項控制,即:
咱們可使用一個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的位操做符有一些很是有趣的用例。我強烈但願您在閱讀本文的過程當中得到的看法從如今起用在你的平常開發中。