點燈坊

學而時習之,不亦悅乎

深入探討 Bitwise Operator

Sam Xiao's Avatar 2019-11-30

有些時候二進位表示法非常好用,如要設定 User 權限是否有 新增修改刪除查詢權限,若每個權限都用二進位的 1 個 bit 表示,0b1111 就表示 4 種權限都有,而 0b1010 則表示只有 新增刪除 權限,以此類推,但這種二進位表示法,該如何在 ECMAScript 使用呢 ?

Version

macOS Catalina 10.15.1
VS Code 1.40.1
Quokka 1.0.261
ECMAScript 2015

Bitwise Operator

  • & : 對 bit 做 AND 運算
  • | : 對 bit 做 OR 運算
  • ~ : 若 bit 做 NOT 運算
  • ^ : 對 bit 做 XOR 運算
  • << : 對 bit 做 SHIFT 運算,如 1 << 2,則相當於 1 向左推兩位,成為 0b100

bit000

所有邏輯的 truth table 如上圖所示。

Mask

ECMAScript 並沒有控制某一個 bit 的語法,若要對某一 bit 做讀寫,主要是靠 mask。

Mask
以二進位表示對某一 bit 的遮罩,如若要對第 2 bit 做處理,其 mask 則為 0b0100
(最右方從 bit 0 開始,非 bit 1)。

Example

設定 user 新增修改刪除查詢 權限,以二進位 4 bit 表示 :

  • 新增 : 以第 3 bit 表示,如 0b1000
  • 修改 : 以第 2 bit 表示,如 0b0100
  • 刪除 : 以第 1 bit 表示,如 0b0010
  • 查詢 : 以第 0 bit 表示,如 0b0001

若 user 只有 新增查詢 權限,則為 0b1001,以此類推。

Read

判斷 user 是否 修改 權限 ?

白話 : 判斷第 2 bit 是否為 1 ?

let data = 0b0101

let fn = flag => (flag & 0b0100) ? 'Bit 2 is enable' : 'Bit 2 is disable';

fn(data) // ?

(flag & 0b0100) 之後,只有第 2 bit 被 mask 過濾出來,若該 bit 為 1,則過濾後為 0b0100,基於 truthy value,所以 (flag & 0b0100)true

bit001

let data = 0b0101

let fn = flag => (flag & (1 << 2)) ? 'Bit 2 is enable' : 'Bit 2 is disable';

fn(data) // ?

也可使用 (1 << 2) 動態產生 mask,再與 flag 做 mask。

bit002

無論使用 mask 或 shift operator 寫法都必須了解,實務上兩種都會看到

判斷 user 是否 修改 權限 ?

白話 : 判斷第 2 bit 是否為 0 ?

let data = 0b0001

let fn = flag => (flag & 0b0100) ? 'Bit 2 is enable' : 'Bit 2 is disable';

fn(data) // ?

(flag & 0b0100) 之後,只有第 2 bit 被 mask 過濾出來,若該 bit 為 0,則過濾後為 0b0000,將 0! not 之後為 1,基於 truthy value,所以 !(flag & 0b0001)true

bit003

let data = 0b0001

let fn = flag => (flag & (1 << 2)) ? 'Bit 2 is enable' : 'Bit 2 is disable';

fn(data) // ?

也可使用 (1 << 2) 動態產生 mask,再與 flag 做 mask。

bit004

Write

讓 user 修改 權限

白話 : 讓第 2 bit 為 1,且其他 bit 不能被修改

let data = 0b0001

let fn = flag => (flag |= 0b0100).toString(2).padStart(4, '0')

fn(data) // ?

bit005

根據真值表得知,任何值 OR 1,其結果必為 1;任何值 OR 0,其結果不變。

因此 flag 只要與 mask 做 OR 運算,該 bit 一定為 1,其餘 bit 不變。

所以 flag = flag | mask,再簡化成 flag |= mask

bit006

let data = 0b0001

let fn = flag => (flag |= (1 << 2)).toString(2).padStart(4, '0')

fn(data) // ?

若沒用 mask,也可使用 (1 << 2) 動態產生 mask,再與 flag 做 mask。

bit007

讓 user 修改 權限

白話 : 讓第 2 bit 為 0,且其他 bit 不能被修改

let data = 0b0101

let fn = flag => (flag &= ~(0b0100)).toString(2).padStart(4, '0')

fn(data) // ?

bit008

根據真值表得知,任何值 AND 0,其結果必為 0;任何值 AND 1,其結果不變。

因此 flag 只要與 mask 做 AND 運算,該 bit 一定為 1,其餘 bit 不變。

所以 flag = flag &= ~mask,再簡化成 flag &= ~mask

bit009

let data = 0b0101

let fn = flag => (flag &= ~(1 << 2)).toString(2).padStart(4, '0')

fn(data) // ?

若沒用 mask,也可使用 (1 << 2) 動態產生 mask,再與 flag 做 mask。

bit010

若 user 修改權限,則改成 修改權限,若 修改權限,則改成 修改權限

白話 : 若第 2 bit 為 0,則 toggle 為 1,若為 1,則 toggle 為 0

let data = 0b0101

let fn = flag => (flag ^= 0b0100).toString(2).padStart(4, '0')

fn(data) // ?

bit012

0 ^ 101 ^ 10,也就是任何數與 1 XOR,剛好都 toggle 成另外一個值。

0 ^ 001 ^ 00,也就是任何數與 0 XOR,結果都不變。

因此 flag 只要與 mask 做 XOR 運算,該 bit 一定為 toggle,其餘 bit 不變。

bit011

let data = 0b0101

let fn = flag => (flag ^= (1 << 2)).toString(2).padStart(4, '0')

fn(data) // ?

若沒用 mask,也可使用 (1 << 2) 動態產生 mask,再與 flag 做 mask。

bit013

Conclusion

  • 本文雖然使用 ECMAScript 為範例,但 bitwise operator 事實上來自於 C 語言,且眾多語言都有支援
  • 無論使用 mask 或 shift operator 寫法都必須了解,實務上兩種都會看到
  • 這種寫法在 C 語言與 Verilog 經常使用,因為 IC 暫存器為了省空間,都是以二進位方式儲存,因此 firmware 必須使用 bitwise operator 去解析暫存器的資料;在高階語言則較少使用,但因為 ECMAScript 也提供 bitwise operator,所以也可以用這種方式解析二進位資料