很多高級語言的開發者都容易忽略位運算符的使用技巧,因為他們總感覺位運算符是底層開發的專利;其實,這是錯誤的,通過巧妙地使用位運算符,常常可以獲取性能上的提升和代碼的精簡;為了在 JavaScript 中充分利用位運算符,我挖掘了一些實用的技巧;本文就集中講解下位運算符在 JavaScript 中的巧妙使用方法及其原理;
提示:如果想忽略基礎知識,直接看技巧,可從 五、取整
開始看起
目錄
- 一、概念
- 機器數
- 真值
- 原碼
- 反碼
- 補碼
- 二、位運算符的特性
- 三、位運算符介紹
- 四、特殊的值
- 五、取整
- 方式1:與0進行或運算
x|0
- 方式2:與-1進行與運算
x&-1
- 方式3:與0異或
x^0
- 方式4:雙非運算
~~x
- 方式5:與同一個數兩次異或
x^a^a
- 方式6:左移0位
x<<0
- 方式7:有符號右移0位
x>>0
- 方式8:對正數無符號右移0位
x>>>0
- 方式9:無符號右移0位再與0進行或運算
x>>>0|0
- 方式10:無符號右移0位再與0進行異或運算
x>>>0^0
- 方式1:與0進行或運算
- 六、與2的冪相乘
- 七、位開關
- 八、求對2的冪的余數
- 九、奇偶判斷
- 十、交互兩個數
- 方法1:中間變量
- 方法2:表達式暫存
- 方法3:異或運算
內容
一、概念
在講解位算符的技巧和原理之前,需要選了解一下一些概念:
-
機器數:
一個數在計算機中的二進制表示形式, 叫做這個數的機器數。機器數是帶符號的,在計算機用一個數的最高位存放符號, 正數為0, 負數為1;
比如,十進制中的數 +3 ,計算機字長為8位,轉換成二進制就是00000011。如果是 -3 ,就是 10000011 。
那么,這里的 00000011 和 10000011 就是機器數。 -
真值:
因為第一位是符號位,所以機器數的形式值就不等于真正的數值。例如上面的有符號數 10000011,其最高位1代表負,其真正數值是 -3 而不是形式值131(10000011轉換成十進制等于131)。所以,為區別起見,將帶符號位的機器數對應的真正數值稱為機器數的真值。
例:0000 0001的真值 = +000 0001 = +1,1000 0001的真值 = –000 0001 = –1
; -
原碼:
原碼就是符號位加上真值的絕對值, 即用第一位表示符號, 其余位表示值.
比如:假設總共有8位:+1 的原碼 = 0000 0001 -1 的原碼 = 1000 0001
-
反碼:
反碼的表示方法是:- 正數的反碼是其原碼;
- 負數的反碼是在其原碼的基礎上, 符號位不變,其余各個位取反;
+1 : 反碼是 [00000001],原碼是 [00000001] -1 : 反碼是 [11111110],原碼是 [10000001]
-
補碼:
補碼的表示方法是:- 正數的補碼就是其本身;
- 負數的補碼是在其原碼的基礎上, 符號位不變, 其余各位取反, 然后加 1;即:在反碼的基礎上加 1;
+1 : 原碼是 [00000001],反碼是 [00000001],補碼是 [00000001] -1 : 原碼是 [10000001],反碼是 [11111110],補碼是 [11111111]
二、位運算符的特性
在 JavaScript 中,位運算符具備以下特性:
- 操作數被轉換成有符號32位整數,用比特序列(0和1組成)表示。超過32位的數字會被丟棄。
例如, 以下具有32位以上的整數將轉換為32位整數:轉換前: 11100110111110100000000000000110000000000001 轉換后: 10100000000000000110000000000001
- 位運算符操作的是操作數對應的機器數,而不是原碼;
- 數值的機器數就是數值原碼對應的補碼;也就是說,位運算符操作的是操作數對應的補碼;
- JavaScript 中的數字類型是 基于 IEEE 754 標準的雙精度 64 位二進制格式的值(
-(2**63 -1)
到2**63 -1
)。但其能表示的最小值 和 最在值 是要比-(2**63 -1)
和2**63 -1
的絕對值大很多的,因為 JavaScript 中的數字可以用 科學計數法xEy
來表示;
三、位運算符介紹
運算符 | 用法 | 描述 |
---|---|---|
按位與(AND) | a & b |
對于每一個比特位,只有兩個操作數相應的比特位都是1時,結果才為1,否則為0。 |
按位或(OR) | a | b |
對于每一個比特位,當兩個操作數相應的比特位至少有一個1時,結果為1,否則為0。 |
按位異或(XOR) | a ^ b |
對于每一個比特位,當兩個操作數相應的比特位有且只有一個1時,結果為1,否則為0。 |
按位非(NOT) | ~ a |
反轉操作數的比特位,即0變成1,1變成0。 |
左移(Left shift) | a << b |
將 a 的二進制形式向左移 b (< 32) 比特位,右邊用0填充。 |
有符號右移 | a >> b |
將 a 的二進制表示向右移 b (< 32) 位,丟棄被移出的位。 |
無符號右移 | a >>> b |
將 a 的二進制表示向右移 b (< 32) 位,丟棄被移出的位,并使用 0 在左側填充。 |
四、特殊的值
下面羅列了一些常用的特殊的值的 及 其對應的機器數 和 其獲取方式;
-
0
:00000000 00000000 00000000 00000000
;機器數的所有位都是 0 ;可通過~(-1)
獲取 -
-1
:11111111 11111111 11111111 11111111
; 機器數的所有位都是 1 ; 可通過~0
或~NaN
獲取; -
-2147483648
:10000000 00000000 00000000 00000000
;只有第一位(符號位)是 1 ,其它位都是 0 ; 可通過1<<31
獲取; -
2147483647
:01111111 11111111 11111111 11111111
; 只有第一位(符號位)是 0 ,其它位都是 1 ; 可通過-1>>>1
或~(1<<31)
獲得;
五、取整
去掉浮點數的小數部分稱為取整;
我們常用的取整方式如下:
- 通過
parseInt(x)
函數; - 對正數使用
Math.ceil(x)
向下舍入; - 對負數使用
Math.floor(x)
向上舍入;
這些函數都是通過對操作數應用一定算法來實現的;下面提供的這些用位運算符取整的方法,要比這些高效得多,且簡潔;
原理:
上面說了,所有的位運算符都會把操作數轉為 32 位整數;所以,當位運算符作用于浮點數時,就會將和浮點數轉為32位整數; 這是基本原理,但并不是所有的位運算符都適合用于取整操作,因為有些運算符的操作過程是不可逆的,或者會改變操作數的整數部分的值;下面是我 設計 并 挑選 的幾個可用于取整的操作;
方式1:與0進行或運算 x|0
將一個數 x 通過與 0 進行或運算 x ^ 0
,可直接得到 x 的整數部分;
示例:
4.3 | 0 // 4
-4.6 | 0 // -4
原理:
根據上表中 或 定義,可得:1 | 0 == 1
和 0 | 0 == 0
;
即:任何位 x 與 0 或 的值都與原來位 x 相等;
而數值 0 對應的機器數是 32 個 0 ,所以,與 0 或的位操作運算導致了 操作數 去掉了小數部分,而且不會改變操作數的整數部分的值;從而實現了取整的效果;
方式2:與-1進行與運算x&-1
將一個數 x 通過與 -1 進行與運算 x & -1
,可直接得到 x 的整數部分;
示例:
4.3 & -1 // 4
-4.6 & -1 // -4
原理:
根據上表中 與 定義,可得:1 & 1 == 1
和 0 & 1 == 0
;
即:任何位 x 和 1 做 與運算 的值都與原來位 x 相等;
而數值 -1 對應的機器數是 32 個 1 ,所以,與 -1 與的位操作運算導致了 操作數 去掉了小數部分,而且不會改變操作數的整數部分的值;從而實現了取整的效果;
方式3:與0異或 x^0
將一個數 x 通過與 0 異或 x ^ 0
,可直接得到 x 的整數部分;
示例:
4.3 ^ 0 // 4
-4.6 ^ 0 // -4
原理:
根據上表中 異或 定義,可得:1 ^ 0 == 1
和 0 ^ 0 == 0
;
即:任何位 x 與 0 異或 的值都與原來位 x 相等;
而數值 0 對應的機器數是 32 個 0 ,所以,與 0 異或的位操作運算導致了 操作數 去掉了小數部分,而且不會改變操作數的整數部分的值;從而實現了取整的效果;
方式4:雙非運算 ~~x
對一個數 x 進行兩次非運算,可直接得到該數 x 的整數部分;
示例:
~~4.3 // 4
~~(-4.6) // -4
原理:
非的位操作符使操作數去掉了小數部分,而雙非操作會使操作數的整數部分反轉之后再反轉從而變回了原來的整數值;
方式5:與同一個數兩次異或 x^a^a
對一個數 x 進行兩次非運算,可直接得到該數 x 的整數部分;
示例:
4.3^5^5 // 4
-4.6^-3^-3 // -4
原理:
根據上表中 異或 定義,可得:
1 ^ 0 == 1
0 ^ 0 == 0
1 ^ 1 == 0
0 ^ 1 == 1
即:
- 任何位 x 與 0 異或 的值 都與原來位 x 相等;
- 任何位 x 與 1 異或 的值 都與原來位 x 相反;
所以:
- 任何位 x 與 0 異或兩次 的值 都與原來位 x 相等;
- 任何位 x 與 1 異或兩次 相當于將原來位 x 反轉兩次,即:與原來位 x 相等;
所以:
- 任何數 x 與 同一個數 異或兩次 的值 都與原來數 x 相等;
所以:異或的位操作符使操作數去掉了小數部分,而與同一個數雙異或的操作會使操作數的整數部分的值不變;
方式6:左移0位 x<<0
將一個數 x 左移0位,可直接得到該數 x 的整數部分;
示例:
4.3<<0 // 4
-4.6<<0 // -4
原理:
左移的位操作符使操作數去掉了小數部分,而左移0位相當于沒有移位,也使得保留了操作數的整數部分;
方式7:有符號右移0位 x>>0
對一個數 x 進行有符號右移0位,可直接得到該數 x 的整數部分;
示例:
4.3>>0 // 4
-4.6>>0 // -4
原理:
有符號右移的位操作符使操作數去掉了小數部分,而有符號右移0位相當于即沒有移位也沒有改變操作數的符號位,也使得保留了操作數的整數部分;
方式8:對正數無符號右移0位 x>>>0
對一個正數 x 進行無符號右移0位,可直接得到該數 x 的整數部分;
示例:
4.3>>>0 // 4
原理:
無符號右移的位操作符使操作數去掉了小數部分,而無符號右移0位相當于沒有移位,即:不會改變操作數的機器數;但是會使操作數變成正數(即:計算機會把該機器數作為一個正數來看代);不過,當操作數本身就是正數時,無符號右移動 0 位就相當于不會改變操作數的正負性,所以也就實現了 保留正數操作數的整數部分;
方式9:無符號右移0位再與0進行或運算 x>>>0|0
對一個數 x 進行無符號右移0位后,再與0進行或運算,可得到該數 x 的整數部分;
示例:
4.3>>>0|0 // 4
-4.6>>>0|0 // -4
原理:
與 x>>>0^0
的原理相同:無符號右移的位操作符使操作數去掉了小數部分,而無符號右移0位相當于沒有移位,即:不會改變操作數的機器數;但是會使操作數變成正數(即:計算機會把該機器數作為一個正數來看代);為了讓計算機把該機器數當作一個有符號32位整數,需要對該機器數應用一次無變化位運算,比如:與0相或;這樣,便可把機器數所表示的值恢復成原來的值;所以,所以也就實現了 保留操作數的整數部分;
方式10:無符號右移0位再與0進行異或運算 x>>>0^0
對一個數 x 進行無符號右移0位后,再與0進行異或運算,可得到該數 x 的整數部分;
示例:
4.3>>>0^0 // 4
-4.6>>>0^0 // -4
原理:
與 x>>>0^0
的原理相同:無符號右移的位操作符使操作數去掉了小數部分,而無符號右移0位相當于沒有移位,即:不會改變操作數的機器數;但是會使操作數變成正數(即:計算機會把該機器數作為一個正數來看代);為了讓計算機把該機器數當作一個有符號32位整數,需要對該機器數應用一次無變化位運算,比如:與0異或;這樣,便可把機器數所表示的值恢復成原來的值;所以,所以也就實現了 保留操作數的整數部分;
六、與2的冪相乘
求一個數 x 與 2 的 n 次方的冪 乘積 除了 用算術運算符 x * (2 ** n)
的方式外,也可用位運算符 向 左移 n 位 的方式:
x << n
示例
5 << 3 // 求 5 * 2 的 3 次方的 冪
原理
對于 二進制的 移位操作,每向左移一位,就相當于乘了 基數,即 2 ,左移 n 位,就相當于 乘了 n 次 基數,即 基數的 n 次方,也即 2 的 n 次方,所以, 一個數 左移 n 位,就相當于 乘了 2的 n次方;
七、位開關
定義(個人定義,非官方):位開關 就是 用二進制數中一位來表示一個二態量;
假如,我們需要表示4個燈泡 A、B、C、D 的狀態;
通常,比較直觀的設計方案是下面這樣:
- 用4個布爾變量 a、b、c、d 分別表示 4 個燈泡的狀態;
- 當布爾值為 true 時,表示燈是在亮著,當布爾值為 false 時,表示燈在關著;
但是,這樣設計的問題是:當我們需要傳遞 這些燈泡的狀態時,我們需要傳遞 4 個參數,比較麻煩;
我們改用下面這個設計方案:
- 用一個數 bulb 的一個二進制位來表示一個燈泡的狀態,比如:0 表示關、1 表示開;
- 4個燈泡的狀態,則只需要一個4位二進制位序列 DCBA 即可表示,如:二進制數
0101
則表示:燈D:關;燈C:開;燈B:關;燈A:開;
這樣,我們只需要用一個參數 就可以傳遞這些燈泡的狀態;
獲取某個燈的狀態:
如果我們想知道某個燈泡(比如 燈B)的狀態,我們可以根據之前定義的位序列 DCBA 來寫出 燈B 的位掩碼 mask = 0b0010
,然后將 表示燈狀態的數 bulb 與 位掩碼 mask 做 位與運算 bulb & mask
得到,如果值不為 0 ,則表示 燈B 的狀態是 開的;
判斷在某幾燈中,是否至少有一個是開著的:
如果我們想知道某幾個燈泡(比如 燈C、燈B)是否至少一個是開著的,我們可以根據之前定義的位序列 DCBA 來寫出 燈C、燈B 的位掩碼 mask = 0b0110
,然后將 表示燈狀態的數 bulb 與 位掩碼 mask 做 位與運算 bulb & mask
得到,如果 值不為 0 ,則表示 燈C 和 燈B 至少一個是開著的;
判斷某些燈是否是都開著的:
如果我們想知道某幾個燈泡(比如 燈C、燈B)是否是都開著的,我們可以根據之前定義的位序列 DCBA 來寫出 燈C、燈B 的位掩碼 mask = 0b0110
,然后將 表示燈狀態的數 bulb 與 位掩碼 mask 做 位與運算,再將運算的結果 與 位掩碼 mask 做相等比較 (bulb & mask) == mask
,如果值不為 true
,則表示 燈C 和 燈B 都是開著的;
總結
用一個二進制位來表示一個 二態量,則多個二態量便可以用一個二進制位序列來表示;這個二進制位序列可以作為一個數存儲在一個變量中;我將表示這樣二進制序列的數叫做 開關序列;
通過與 開關序列 做 與
運算 來獲取部分二態量信息的二進制序列 叫做 這些二態量的 掩碼;掩碼 的表示方法是:將不需要的二態量的二進制位置為 0 ,將需要的二態量置為 1 ,這樣得到的一個二進制位序列 就是 這些所需量 的 掩碼;
獲取某個二態量的值
將 開關序列 與 該二態量的 掩碼 做 位與 運算,運算的結果便是該二態量的值;
switchSeq & mask
示例:
var switchSeq = 0b0110; //包含多個二態量入信息的開關序列
var mask = 0b0010; //第二個二態量的掩碼
var second = switchSeq & mask; //將 開關序列 與 該二態量的 掩碼 做 位與 運算 來 獲取第二個二態量的值
原理:
根據上表中 與 定義,可得:1 & 1 == 1
和 0 & 1 == 0
;
即:任何位 x 和 1 做 與運算 的值都與原來位 x 相等;任何位 x 和 0 做 與運算 的值是 0;
而 掩碼中 不需要的二態量的二進制位被置為了 0 ,在與 開關序列 相與 時,不需要的位就被 置為了 0 ;掩碼中 需要的二態量置為 1 ,在與 開關序列 相與 時,需要的位的值 就被保留了下來 ;所以,開關序列 與 某個二態量的 掩碼 做 與運算 后,得到的值就是 該二態量的值;
判斷是否至少包含某些二態量中的一個
將 開關序列 與 這些二態量的 掩碼 做 位與 運算 ,如果結果不為 0 ,則表示 開關序列 中 包含這些二態量中的至少一個;如果結果 為 0 ,則表示 開關序列中 不包含 這些二態量中的任何一個;
switchSeq & mask
示例:
var switchSeq = 0b0110; //包含多個二態量入信息的開關序列
var mask = 0b0011; //第一、二個二態量的掩碼
var firstOrSecond = switchSeq & mask; //將 開關序列 與 第一、二個二態量的 掩碼 做 位與 運算 ,如果結果不為 0 ,則表示 開關序列 中 包含第一、二個二態量中的至少一個
原理:
根據上表中 與 定義,可得:1 & 1 == 1
和 0 & 1 == 0
;
即:任何位 x 和 1 做 與運算 的值都與原來位 x 相等;任何位 x 和 0 做 與運算 的值是 0;
而 掩碼中 不需要的二態量的二進制位被置為了 0 ,在與 開關序列 相與 時,不需要的位就被 置為了 0 ;掩碼中 需要的二態量置為 1 ,在與 開關序列 相與 時,需要的位的值 就被保留了下來 ;所以,開關序列 與 某個二態量的 掩碼 做 與運算 后,得到的值就是 該二態量的值;
判斷是否包含某些二態量中的每一個
將 開關序列 與 這些二態量的 掩碼 做 位與 運算 ,再讓 運算結果 與 掩碼 相 相等比較,如果相等,則表示 開關序列 包含這些二態量中的每一個;如果不相等 ,則表示 開關序列中 并不包含 這些所有的二態量;
(switchSeq & mask) === mask
示例:
var switchSeq = 0b0110; //包含多個二態量入信息的開關序列
var mask = 0b0011; //第一、二個二態量的掩碼
var all = (switchSeq & mask) === mask; //將 開關序列 與 第一、二個二態量的 掩碼 做 位與 運算 然后再 與 掩碼 做相等比較,如果相等,則表示 開關序列 包含這些二態量中的每一個;如果不相等 ,則表示 開關序列中 并不包含 這些所有的二態量;
原理:
根據上表中 與 定義,可得:1 & 1 == 1
和 0 & 1 == 0
;
即:任何位 x 和 1 做 與運算 的值都與原來位 x 相等;任何位 x 和 0 做 與運算 的值是 0;
而 掩碼中 不需要的二態量的二進制位被置為了 0 ,在與 開關序列 相與 時,不需要的位就被 置為了 0 ,這與 掩碼 中的值一樣;掩碼中 需要的二態量置為 1 ,在與 開關序列 相與 時,需要的位的值 就被保留了下來 ;所以,如果 開關序列 包含所有這些需要的位,則 掩碼 在與 開關序列 相與 時,需要的位的值 就是 1 ,即:與 掩碼 中的值一樣;所以,如果 開關序列 包含某些二態量中的每一個,則 開關序列 與 這些二態量的 掩碼 做 位與 運算后的值 會和 掩碼 相等;
八、求對2的冪的余數
JavaScript 語言自帶取余運算符 %
,但如果是求 對 2的整數次冪 的余數,則還有另一個更加高效的方法;
求 數x 對 2 的 n 次冪 的余數,可用 數 x 與 從右往左連續 n 個位是 1 的二進制數 mask 做位與運算 x & mask
,運算的結果就是 所求余數;
示例:
求 10 對 2的3次方(也就是8)的余數;
var x = 10;
var mask = 0b111; //因為要求 對 2的3次方的冪的余數,所以 mask 為3個二進制1;
var mod = x & mask; // mod 即為 10 對 2的3次方的冪的 余數
原理:
將 數 x 與 從右往左連續 n 個位是 1 的二進制數 mask 做 位與 運算 x & mask
后,數 x 的 最右邊的 n 位的值就被保留了下來,其它位都置為了 0 ,此時的數值正是小于 2的次方的冪的數,也就是 對 2的n次方的余數;
九、奇偶判斷
判斷一個整數 x 是奇數還是偶數的方法是:求這個數對2的余數 x % 2
,如果是余 零 ,則該數為偶數,如果余數不是零,則該是奇數;
由于 2 是 2 的 1 次方的冪,所以,可以用 求對2的冪的余數 中所述的方法,讓該數 與 1 相 相與 x & 1
,如果結果是 零,則該數是偶數,如果該數不為 零 ,則該數是奇數;
所以,判斷一個數 x
是奇數還是偶數的方法有 2 種:
- 求該數對 2 的余數
x % 2
是否是零;如果是零,則為偶數; - 讓該數 和 1 做 位與運算
x & 1
,如果結果是零,則為偶數;
十、交互兩個數
交互兩個整數變量 a、b 的方法有以下幾種:
方法1:中間變量
var temp = a; // 定義臨時變量,用來臨時保存 a 的原始值;
a = b; // 次 b 賦值給 a;
b = temp; // 將 a 的原始值 賦值給 b;
方法2:表達式暫存
a = [b, b = a][0];
或
a = {a:b, _:b = a}.a;
這種方式是利用 表達式 的計算 順序 將 原始值 暫存 在 數組的元素 或 對象的屬性 中 來 實現 臨時變量 的效果;
方法3:異或運算
a ^= b;
b ^= a;
a ^= b;
數學證明:
上面給出的是代碼;代碼的變量是動態的,數學的變量是靜態的,為了方便證明,先把方面的代碼改成如下等效形式:
等效代碼:
t = a ^ b;
b = t ^ b;
a = t ^ b;
然后用數學靜態變量的方式來等效表示上面的等式,即:用 a、b 表示代碼中初始的 a、b 值;用 x 表示 代碼中最終 a 的值,用 y 表示代碼中最終的 b 的值;
等效的數學表達式:
t = a ^ b;
y = t ^ b;
x = t ^ y;
用數學符號表示:
t = a⊕b
y = t⊕b
x = t⊕y
證明之前,需要選證明一個公式:
(a⊕b)⊕b = a
即: a 與 b 異或的值 再與 b 異或,最終的結果 等于 a ;
(a⊕b)⊕b = a
證明過程:
所以,就有:
所以:
y = a
x = b
又因為 x 表示 代碼中最終 a 的值,y 表示代碼中最終的 b 的值;
所以,對于代碼
t = a ^ b;
b = t ^ b;
a = t ^ b;
執行完成后,最終 b 的值 是 原來 a 的值,最終 a 的值,是原來 b 的值,從而實現了 變量 a 和 b 的值互換的操作;