代計算機的數(shù)據(jù)是以二進制的形式進行存儲的。 位運算就是直接對整數(shù)在內(nèi)存中的二進制位進行操作。使用位運算可以提高代碼性能,精簡代碼。
在說位運算前,先簡單說說幾個概念,具體有機器數(shù)、真值、原碼、反碼、補碼
一個數(shù)在計算機中的二進制表示形式,叫做這個數(shù)的機器數(shù)。
機器數(shù)是帶符號的,計算機中機器數(shù)的最高位是符號位, 正數(shù)為0, 負數(shù)為1。
二進制的位數(shù)受機器設(shè)備的限制。機器內(nèi)部設(shè)備一次能表示的二進制位數(shù)叫機器的字長,一臺機器的字長是固定的。字長8位叫一個字節(jié)(Byte),機器字長一般都是字節(jié)的整數(shù)倍,如字長8位、16位、32位、64位。
例如在8位機器中,+3轉(zhuǎn)為二進制為 00000011,-3轉(zhuǎn)為二進制為 10000011,其中00000011和10000011就是機器數(shù)。
直接用正號“+”和負號“-”來表示其正負的二進制數(shù)叫做機器數(shù)的真值。
例如“00000011”和“10000011”是兩個機器數(shù),而它們的真值分別為+0000011(即+3)和-0000011(即-3)。
將數(shù)的真值形式中“+”號用“0”表示,“-”號用“1”表示時,叫做數(shù)的原碼形式,簡稱原碼。
也可理解為符號位加上真值的絕對值。第一位是符號位,其余位表示數(shù)值。
例如:
十進制 | 真值 | 原碼 |
+3 | +0000011 | 00000011 |
-3 | -0000011 | 10000011 |
原碼的表示比較直觀,但是在進行加減法運算時,由于符號位的存在,使得機器的運算比較復(fù)雜,所以就引入了反碼和補碼。
對于正數(shù)來說,其反碼和原碼完全相同。
對于負數(shù)來說,其反碼是在其原碼的基礎(chǔ)上, 符號位不變,其他位取反(0變1,1變0)。
十進制 | 真值 | 原碼 | 反碼 |
+3 | +0000011 | 00000011 | 00000011 |
-3 | -0000011 | 10000011 | 11111100 |
對于正數(shù)來說,其補碼和原碼完全相同。
對于負數(shù)來說,其補碼是其反碼的末位加1,也就是說在其原碼的基礎(chǔ)上, 符號位不變,其他位取反后加1。
十進制 | 真值 | 原碼 | 反碼 | 補碼 |
+3 | +0000011 | 00000011 | 00000011 | 00000011 |
-3 | -0000011 | 10000011 | 11111100 | 11111101 |
注意:在計算機系統(tǒng)中,數(shù)值一律用補碼來表示和存儲。主要原因是為了便于處理和運算。
具體為何需要引入反碼和補碼以及為何計算機系統(tǒng)中數(shù)值用補碼來表示和存儲,文章篇幅有限,此塊也不是本文的重點,詳細可以參考這篇文章www.cnblogs.com/zhangziqiu/…
? 在 JavaScript 中,數(shù)值是以64位浮點數(shù)的形式儲存。但是位運算符只對整數(shù)起作用。JavaScript在做位運算時,會事先將其轉(zhuǎn)換為32位有符號整型(用比特序列表示,即0和1組成)并開始計算,在得到結(jié)果后再將其轉(zhuǎn)回JavaScript的數(shù)值類型。
? 因為 js 的整數(shù)默認是帶符號數(shù),所以在位運算中,只能使用 31 位,開發(fā)者是不能訪問最高位的。在運算時,如果位數(shù)超過,那么超過的數(shù)字會被丟棄,這會造成各種錯誤的結(jié)果,并且沒有任何警告信息。所以,并不是所有計算都適合通過位運算符來計算。
? 位運算符操作的是數(shù)值對應(yīng)的機器數(shù),這個機器數(shù)的表示形式就是補碼的形式。(上面說了,為了便于處理和運算,計算機系統(tǒng)中,數(shù)值用補碼來表示和存儲。)
? JavaScript中的位運算符有以下:
console.log(5 & 3) // 輸出1
// 5的機器數(shù)表示為 00000000 00000000 00000000 00000101
// 3的機器數(shù)表示為 00000000 00000000 00000000 00000011
// 所以計算結(jié)果為1 00000000 00000000 00000000 00000001
:或運算;兩個運算比較的bit位只要有一個是1,這個bit位就是1
console.log(5 | 3) // 輸出7
// 5的機器數(shù)表示為 00000000 00000000 00000000 00000101
// 3的機器數(shù)表示為 00000000 00000000 00000000 00000011
// 所以計算結(jié)果為7 00000000 00000000 00000000 00000111
^ :異或運算;兩個運算比較的bit位相同則為0,不同則為1
console.log(5 ^ 3) // 輸出6
// 5的機器數(shù)表示為 00000000 00000000 00000000 00000101
// 3的機器數(shù)表示為 00000000 00000000 00000000 00000011
// 所以計算結(jié)果為6 00000000 00000000 00000000 00000110
~:取反運算;0變1,1變0
console.log(~5) // 輸出-6
// 5的機器數(shù)表示為 00000000 00000000 00000000 00000101
// 取反之后表示為 11111111 11111111 11111111 11111010
// 該數(shù)是負數(shù),負數(shù)補碼與原碼不同,先將其轉(zhuǎn)為原碼的反碼:11111111 11111111 11111111 11111001
// 再轉(zhuǎn)為原碼:10000000 00000000 00000000 00000110
// 所以結(jié)果為:-6
<< :左移;各二進位全部左移若干位,高位丟棄(即左邊超出的丟棄),低位補0(即右邊差的)
console.log(5 << 3) // 輸出40
// 5的機器數(shù)表示為 00000000 00000000 00000000 00000101
// 左移三位結(jié)果為40 00000000 00000000 00000000 00101000
圖示:
console.log(-1073741822 << 2) // 輸出8
// -1073741822的機器數(shù)表示為 11000000 00000000 00000000 00000010
// 左移兩位結(jié)果為8 00000000 00000000 00000000 00001000
圖示:
>> :帶符號擴展右移;各二進位全部右移若干位,右邊被移出的丟棄,左邊會復(fù)制最左側(cè)的位來填充左側(cè)(這里最左側(cè)的位是符號位,也可以認為對于無符號數(shù),高位補 0,對于有符號數(shù),高位補符號位)。
console.log(5 >> 2) // 輸出1
// 5的機器數(shù)表示為 00000000 00000000 00000000 00000101
// 帶符號擴展右移兩位結(jié)果為1 00000000 00000000 00000000 00000001
圖示:
console.log(-5 >> 2) // 輸出-2
// -5的機器數(shù)表示為: 11111111 11111111 11111111 11111011
// 帶符號擴展右移兩位結(jié)果為: 11111111 11111111 11111111 11111110
// 先將該值轉(zhuǎn)為原碼的反碼: 11111111 11111111 11111111 11111101
// 再轉(zhuǎn)為原碼: 10000000 00000000 00000000 00000010
// 所以結(jié)果為:-2
圖示:
>>> :無符號右移;各二進位全部右移若干位,右邊被移出的丟棄,左邊用0填充。因為符號位變成了 0,所以結(jié)果總是非負數(shù)。(即便右移 0 個比特,結(jié)果也是非負的。)
console.log(5 >>> 2) // 輸出1
// 5的機器數(shù)表示為 00000000 00000000 00000000 00000101
// 無符號右移兩位結(jié)果為1 00000000 00000000 00000000 00000001
圖示:
console.log(-5 >>> 2) // 輸出1073741822
// -5的機器數(shù)表示為: 11111111 11111111 11111111 11111011
// 無符號右移兩位結(jié)果為1073741822: 00111111 11111111 11111111 11111110
根據(jù)數(shù)值的機器數(shù)最后一位是0還是1即可判斷,為0就是偶數(shù),為1就是奇數(shù)。
function fun(num) {
if ((num & 1)===0) {
return "偶數(shù)";
}
if ((num & 1)===1) {
return "奇數(shù)";
}
}
console.log(fun(44)); // 輸出偶數(shù)
console.log(fun(55)); // 輸出奇數(shù)
不使用第三個變量,實現(xiàn)兩個數(shù)交換,可以通過以下方式實現(xiàn)
a=a + b;
b=a - b;
a=a - b;
// 或者
a=[b, (b=a)][0];
// 或者
[a, b]=[b, a]
根據(jù)異或運算的規(guī)則,可以得出一些特性
// a取任何值,下面的結(jié)果都成立
a^a=0
a^0=a
// 異或運算且滿足交換律和結(jié)合律,即
a^b=b^a
a^b^c=a^(b^c)=(a^c)^b
// 其實與運算也是滿足交換律和結(jié)合律的
a&b=b&a
a&b&c=a&(b&c)=(a&c)&b
// 于是就可以推出
a^b^a=b^(a^a)=b^0=b
// 如果 c=a^b 那么 a=c^b,這是因為 a=c^b=a^b^b=a^(b^b)=a^0=a
// 如果 d=a^b^c 那么 a=d^b^c, 以此類推...
那么根據(jù)以上的特性,就可以用位運算的方式實現(xiàn)兩數(shù)交換,如下
a=a^b
b=a^b
a=a^b
將某個數(shù)值帶符號右移一位,相當(dāng)于該數(shù)值除以2;
將某個數(shù)值左移一位,相當(dāng)于該數(shù)值除以2;
console.log(6 >> 1) // 輸出3
console.log(6 << 1) // 輸出12
而右移n位,就相當(dāng)于該數(shù)值除以2的n次方
而左移n位,就相當(dāng)于該數(shù)值乘以2的n次方
console.log(32 >> 3) // 輸出4, 相當(dāng)于32除以2的3次方(8),等于4
console.log(32 << 3) // 輸出256, 相當(dāng)于32乘以2的3次方(8),等于256
但是用這種用法實現(xiàn)乘除法有很多問題,比如
console.log(7 >> 1) // 輸出3
如果數(shù)值操作影響到了符號位,結(jié)果會錯誤
console.log(-2147483646 << 1) // 輸出4, 而-2147483646除以2的結(jié)果應(yīng)該是-1073741823
console.log(5.2 | 0) // 輸出5
console.log(-7.2 | 0) // 輸出-7
通過與運算實現(xiàn):將數(shù)值與-1進行或運算從而得到該數(shù)值的整數(shù)部分
console.log(5.2 & -1) // 輸出5
console.log(-7.2 & -1) // 輸出-7
通過異或運算實現(xiàn):
console.log(5.2 ^ 0);// 輸出5
console.log(-7.2 ^ 0);// 輸出-7
將數(shù)值與另一個數(shù)進行兩次異或運算
console.log(5.2 ^ 2 ^ 2); // 輸出5
console.log(-7.2 ^ 2 ^ 2);// 輸出-7
通過取反運算實現(xiàn):將數(shù)值進行兩次取反運算
console.log(~~5.2); // 輸出5
console.log(~~-7.2); // 輸出-7
通過左移或右移運算實現(xiàn):將數(shù)值左移或右移0位
console.log(5.2 >> 0); // 輸出5
console.log(-7.2 << 0); // 輸出-7
位掩碼(BitMask),是”位(Bit)“和”掩碼(Mask)“的組合詞。”位“指代著二進制數(shù)據(jù)當(dāng)中的二進制位,而”掩碼“指的是一串用于與目標數(shù)據(jù)進行按位操作的二進制數(shù)字。組合起來,就是”用一串二進制數(shù)字(掩碼)去操作另一串二進制數(shù)字“的意思。
位掩碼可以用于多種場景,比較常用的,我們可以用它來進行權(quán)限管理。
假設(shè)我們在做一個直播教學(xué)系統(tǒng),每個學(xué)生都有其自己的狀態(tài),比如是否舉手,攝像頭狀態(tài),設(shè)備類型三個狀態(tài)。
如果我們給每個學(xué)生設(shè)置三個狀態(tài)值,那會很麻煩(這里只是舉例三個,現(xiàn)實場景可能有七八個狀態(tài)值)
那么我們可以設(shè)計每個學(xué)生一個狀態(tài)碼,例如這個狀態(tài)碼是一個9位的二進制數(shù)
我們可以這個數(shù)上劃分每個狀態(tài)所占的位
例如從最低位(按最低位0)開始
具體可以在代碼中定義一個枚舉:
const enums={
// 舉手狀態(tài)
handsUp: {
open: 0b01, // 舉手中
close: 0b10, // 未舉手
unknow: 0b00, // 未知
lowBitPos: 0, // 從第幾位開始使用
bitLength: 2, // 所占位的長度
},
// 攝像頭狀態(tài)
camera: {
open: 0b001, // 打開
close: 0b010, // 關(guān)閉
disable: 0b011, // 設(shè)備故障
ban: 0b100, // 被禁用
unknow: 0b000, // 未知
lowBitPos: 2, // 從第幾位開始使用
bitLength: 3, // 所占位的長度
},
// 設(shè)備類型
device: {
window: 0b0001, // window電腦
mac: 0b0010, // 蘋果電腦
ipad: 0b0011, // 蘋果平板
android: 0b0100, // 安卓手機
ios: 0b0111, // 蘋果手機
unknow: 0b0000, // 未知
lowBitPos: 5, // 從第幾位開始使用
bitLength: 4, // 所占位的長度
}
};
例如我隨便舉個例子,假設(shè)某個學(xué)生的狀態(tài)碼為 0b001001001
那么其對應(yīng)狀態(tài)如下
但是在代碼中,我們要如何獲取或修改學(xué)生狀態(tài)對應(yīng)的值呢?這就要用到前面所說的位掩碼的知識了
首先是獲取狀態(tài)值,我們需要從該狀態(tài)碼中截取出指定的從某一位到某一位的值。
// 舉個例子,假設(shè)學(xué)生此時的狀態(tài)碼為0b001001001,獲取該學(xué)生的攝像頭狀態(tài)
let state=0b001001001
// 因為攝像頭狀態(tài)是從低位開始(0開始)第二位到第四位
// 首先將狀態(tài)碼右移兩位
let v=state >> enums.camera.lowBitPos // enums上面定義了
// 然后將轉(zhuǎn)換后的值與0b111進行與運算
let result=v & 0b111
console.log(result) // 輸出結(jié)果為3,也就是0b010,如此便獲取到了該學(xué)生的攝像頭狀態(tài)
上面代碼中,因為攝像頭狀態(tài)是占了三個bit位,所以需要與0b111進行與運算來得到結(jié)果,但是舉手狀態(tài)是兩位,設(shè)備類型是四位,就要分別和0b11和0b1111進行與運算,這樣比較麻煩,所以上面的代碼可以優(yōu)化為
let result=v & ((0b1 << enums.camera.bitLength) - 1)
也可以將所有需要用到的位全是1的值先存在數(shù)組里面,方便后續(xù)運算
const bitArr=[0b0]
// 但位運算時僅支持 1 << 31大小
for (let i=1; i < 32; i++) {
bitArr.push((0b1 << i) - 1)
}
Object.freeze(bitArr) // 凍結(jié)數(shù)組 不允許修改
let result=v & bitArr[enums.camera.bitLength]
總結(jié)上面代碼,我們可以將獲取狀態(tài)值封裝為一個函數(shù)
const getProvide=(state, lowBitPos, bitLength)=> {
return (state >> lowBitPos) & bitArr[bitLength]
}
知道如何獲取以后,那么修改該如何呢
例如,目前學(xué)生的狀態(tài)碼對應(yīng)的設(shè)置狀態(tài)為0b0010(蘋果電腦),我們要修改學(xué)生的設(shè)備狀態(tài)為0b0100(安卓手機)
let state=0b001001001
// 首先先獲取設(shè)備狀態(tài)對應(yīng)的值
let deviceState=getProvide(state, enums.device.lowBitPos, enums.device.bitLength)
// 然后先清除設(shè)備狀態(tài)對應(yīng)的位數(shù)區(qū)間
let clearState=state - (deviceState << enums.device.lowBitPos)
// 然后重新加上新的狀態(tài)區(qū)間即可
let result=clearState + (enums.device.android << enums.device.lowBitPos)
console.log(result.toString(2)) // 輸出 010001001
封裝為函數(shù)如下
const setProvide=(state, lowBitPos, bitLength, newSingleState)=> {
const get=getProvide(state, lowBitPos, bitLength)
let clearState=state - (get << lowBitPos)
return clearState + (newSingleState << lowBitPos)
}
如此便實現(xiàn)了對學(xué)生用戶的權(quán)限狀態(tài)控制。
者:平臺前端組
轉(zhuǎn)發(fā)鏈接:https://mp.weixin.qq.com/s/MbSTkkzf7nDjtBkmh7Iqbw
者:Dunizb
轉(zhuǎn)發(fā)鏈接:https://mp.weixin.qq.com/s/kxo6tOmaJYdINiIOqls1LA
*請認真填寫需求信息,我們會在24小時內(nèi)與您取得聯(lián)系。