位运算

0 阅读12分钟

JavaScript 位运算速查

位运算的核心思想:

> 把整数看成一组二进制 bit,然后对这些 bit 进行操作。

例如:

```text
十进制 5

二进制:
00000101

一个 byte 有 8 个 bit:

bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
  ↓    ↓    ↓    ↓    ↓    ↓    ↓    ↓
 128   64   32   16    8    4    2    1

一、位运算符号说明

1. & 按位与 AND

规则:

0 & 0 = 0
0 & 1 = 0
1 & 0 = 0
1 & 1 = 1

只有两个 bit 都是 1 时,结果才是 1

示例:

5 & 3

二进制:

5 = 0101
3 = 0011

    0101
&   0011
--------
    0001

结果:

1

常见用途:

  • 检查某一位
  • 取某几位
  • 使用 mask 过滤数据

2. | 按位或 OR

规则:

0 | 0 = 0
0 | 1 = 1
1 | 0 = 1
1 | 1 = 1

只要有一个 bit 是 1,结果就是 1

示例:

5 | 3

二进制:

5 = 0101
3 = 0011

    0101
|   0011
--------
    0111

结果:

7

常见用途:

  • 设置某一位为 1
  • 把多个字段组合到一个 byte 中

3. ^ 按位异或 XOR

规则:

0 ^ 0 = 0
0 ^ 1 = 1
1 ^ 0 = 1
1 ^ 1 = 0

口诀:

相同为 0,不同为 1。

示例:

5 ^ 3
    0101
^   0011
--------
    0110

结果:

6

常见用途:

  • 翻转某一位
  • toggle 开关状态

4. ~ 按位取反 NOT

规则:

0 -> 1
1 -> 0

例如:

00001111

取反:

11110000

JavaScript 需要注意:

JavaScript 位运算会转换为 32 位有符号整数,因此:

~0x80

不会直接得到:

0x7F

而会得到一个负数。

但是下面这种写法依然是正确的:

value &= ~0x80;

因为我们关注的是 bit 是否被清除。


5. << 左移

格式:

value << n

表示:

二进制整体向左移动 n 位,右边补 0。

例如:

1 << 3

过程:

00000001

<< 3

00001000

结果:

8

常见用途:

  • 创建 bit mask
  • 把字段移动到指定位置
  • 拼接多个 byte

6. >> 有符号右移

格式:

value >> n

表示:

向右移动 n 位。

例如:

8 >> 2
00001000

>> 2

00000010

结果:

2

常见用途:

  • 把高位字段移动到低位
  • 读取某一段 bit

7. >>> 无符号右移

格式:

value >>> n

>> 的区别是:

>>   会保留符号
>>>  左边永远补 0

常见用途:

value >>> 0

可以把 JavaScript 的 32 位有符号整数转换成无符号数。

例如:

-1 >>> 0

结果:

4294967295

即:

0xFFFFFFFF

二、复合赋值符号

下面几种代码非常常见:

value |= mask;

等价于:

value = value | mask;

value &= mask;

等价于:

value = value & mask;

value ^= mask;

等价于:

value = value ^ mask;

value <<= 2;

等价于:

value = value << 2;

value >>= 2;

等价于:

value = value >> 2;

三、检查 bit n

公式:

const bit = (value & (1 << n)) !== 0;

例如:

const value = 0b10100101;

// 检查 bit7
const isEnd = (value & 0x80) !== 0;

console.log(isEnd);
// true

过程:

value:

10100101

bit7 的 mask:

10000000

也就是:

0x80

执行:

10100101
&
10000000
--------
10000000

结果不为 0,所以:

bit7 = 1

结果:

true

通用写法:

(value & (1 << n)) !== 0

例如检查 bit3:

(value & (1 << 3)) !== 0

因为:

1 << 3

00000001
↓
00001000

所以:

1 << 3 = 0x08

四、设置 bit n = 1

公式:

value |= 1 << n;

例如:

let value = 0b10100001;

// 设置 bit3
value |= 0x08;

过程:

value:

10100001

bit3 mask00001000

执行:

10100001
|
00001000
--------
10101001

结果:

10101001

bit3 被设置成了 1

通用公式:

value |= 1 << n;

五、清除 bit n

公式:

value &= ~(1 << n);

例如:

let value = 0b10100101;

// 清除 bit7
value &= ~0x80;

首先:

0x80

10000000

取反:

~0x80

01111111

然后:

10100101
&
01111111
--------
00100101

所以 bit7 被清零:

10100101
↓
00100101

通用公式:

value &= ~(1 << n);

六、翻转 bit n

公式:

value ^= 1 << n;

例如:

let value = 0b00000001;

// 翻转 bit2
value ^= 0x04;

过程:

00000001
^
00000100
--------
00000101

bit2:

0 -> 1

如果再执行一次:

value ^= 0x04;

过程:

00000101
^
00000100
--------
00000001

bit2:

1 -> 0

所以:

value ^= mask;

很适合做:

toggle

七、取低 n 位

公式:

const result = value & ((1 << n) - 1);

例如:

const value = 0b10110101;

// 取低 4 位
const seq = value & 0x0f;

过程:

value:

10110101

0x0F

00001111

执行:

10110101
&
00001111
--------
00000101

结果:

5

所以:

seq === 5;

为什么 (1 << n) - 1 能生成 mask?

例如:

(1 << 4) - 1

先:

1 << 4

00000001
↓
00010000

即:

16

减 1:

00010000
-
00000001
--------
00001111

所以:

(1 << 4) - 1

得到:

00001111

也就是:

0x0F

规律:

n = 1

(1 << 1) - 1
00000001
n = 2

(1 << 2) - 1
00000011
n = 3

(1 << 3) - 1
00000111
n = 4

(1 << 4) - 1
00001111

所以:

(1 << n) - 1

可以理解成:

生成 n 个连续的 1。


八、读取某一段 bit

公式:

const result =
    (value >> offset) &
    ((1 << width) - 1);

例如协议:

bit7      FIRST
bit6      LAST
bit5~bit4 TYPE
bit3~bit0 SEQ

现在:

const value = 0b10110111;

读取:

bit5~bit4

TYPE 一共占:

2 bit

所以:

const type = (value >> 4) & 0x03;

过程:

value:

10110111

先右移 4 位:

10110111
>> 4
--------
00001011

TYPE 占 2 bit,所以 mask:

00000011

也就是:

0x03

执行:

00001011
&
00000011
--------
00000011

结果:

3

所以:

type === 3;

九、把字段放到指定位置

公式:

const field = value << offset;

例如:

const type = 3;

TYPE 自己的二进制:

00000011

协议规定 TYPE 位于:

bit5~bit4

所以要左移 4 位:

const typeBits = (type & 0x03) << 4;

第一步:

type:

00000011

&
00000011
--------
00000011

第二步:

00000011
<< 4
--------
00110000

最终:

00110000

也就是:

0x30

这里要特别注意:

type & 0x03

表示:

TYPE 自己只能占 2 bit。

而:

0x30

表示:

TYPE 在整个 byte 里面位于 bit5~bit4。

二者并不矛盾:

字段自身 mask:

00000011
= 0x03

移动到 bit5~bit4:

00110000
= 0x30

也就是:

0x03 << 4

结果:

0x30

十、多字段组合成一个 byte

协议:

bit7      FIRST
bit6      LAST
bit5~bit4 TYPE
bit3~bit0 SEQ

数据:

const first = true;
const last = false;
const type = 3;
const seq = 7;

组装:

const seqByte =
    (first ? 0x80 : 0) |
    (last ? 0x40 : 0) |
    ((type & 0x03) << 4) |
    (seq & 0x0f);

分别看。

FIRST:

true

->

10000000

即:

0x80

LAST:

false

->

00000000

TYPE:

type = 3

00000011

<< 4

00110000

SEQ:

seq = 7

00000111

全部组合:

FIRST
10000000

LAST
00000000

TYPE
00110000

SEQ
00000111

执行 OR:

10000000
|
00000000
|
00110000
|
00000111
--------
10110111

所以:

seqByte = 10110111

即:

0xB7

十一、uint16 大端

代码:

const value = (data[0] << 8) | data[1];

例如:

const data = [
    0x12,
    0x34
];

大端数据:

12 34

表示:

0x1234

过程:

data[0]:

0x12

00010010

左移 8 位:

00010010 00000000

第二个 byte:

data[1]:

0x34

00110100

扩展成 16 bit:

00000000 00110100

执行:

00010010 00000000
|
00000000 00110100
-----------------
00010010 00110100

得到:

0x1234

所以:

const value = (data[0] << 8) | data[1];

可以理解为:

第一个 byte 放高 8 位,第二个 byte 放低 8 位。


十二、uint16 小端

代码:

const value = data[0] | (data[1] << 8);

例如:

const data = [
    0x34,
    0x12
];

小端:

34 12

表示:

0x1234

低字节:

data[0] = 0x34

高字节:

data[1] = 0x12

所以:

0x34
|
0x12 << 8

也就是:

00000000 00110100
|
00010010 00000000
-----------------
00010010 00110100

最终:

0x1234

十三、取 uint16 高字节

公式:

const high = (value >> 8) & 0xff;

例如:

const value = 0x1234;

二进制:

00010010 00110100

右移 8 位:

00000000 00010010

然后:

&
00000000 11111111

结果:

00000000 00010010

也就是:

0x12

所以:

const high = (0x1234 >> 8) & 0xff;

// 0x12

十四、取 uint16 低字节

公式:

const low = value & 0xff;

例如:

const value = 0x1234;

执行:

00010010 00110100
&
00000000 11111111
-----------------
00000000 00110100

得到:

0x34

所以:

const low = 0x1234 & 0xff;

// 0x34

十五、转换成 uint32

代码:

const unsigned = value >>> 0;

例如:

const value = -1;

const unsigned = value >>> 0;

console.log(unsigned);

结果:

4294967295

原因是 JavaScript 位运算使用 32 位整数。

-1 的 32 位补码:

11111111 11111111 11111111 11111111

如果把它当作有符号数:

-1

如果把相同 bit 当作无符号数:

4294967295

所以:

value >>> 0

经常用于:

将一个 32 位结果按照 uint32 来解释。


十六、常见 Mask 速查

0x01 = 00000001

用途:

取 bit0

0x03 = 00000011

用途:

取低 2 位

0x07 = 00000111

用途:

取低 3 位

0x0F = 00001111

用途:

取低 4 位

0x1F = 00011111

用途:

取低 5 位

0x3F = 00111111

用途:

取低 6 位

0x7F = 01111111

用途:

取低 7 位

0x80 = 10000000

用途:

检查 / 设置 bit7

0xF0 = 11110000

用途:

取高 4 位

0xFF = 11111111

用途:

取低 8 位

十七、最常用公式速查

检查某一个 bit

const bit = (value & (1 << n)) !== 0;

例如:

const isEnd = (value & 0x80) !== 0;

设置某一个 bit

value |= 1 << n;

例如:

value |= 0x08;

清除某一个 bit

value &= ~(1 << n);

例如:

value &= ~0x80;

翻转某一个 bit

value ^= 1 << n;

例如:

value ^= 0x04;

取低 n 位

const result =
    value & ((1 << n) - 1);

例如:

const seq = value & 0x0f;

读取某一段 bit

const result =
    (value >> offset) &
    ((1 << width) - 1);

例如读取:

bit5~bit4
const type =
    (value >> 4) & 0x03;

把字段放到指定位置

const field =
    (value & mask) << offset;

例如:

const typeBits =
    (type & 0x03) << 4;

多字段组合

const seqByte =
    (first ? 0x80 : 0) |
    (last ? 0x40 : 0) |
    ((type & 0x03) << 4) |
    (seq & 0x0f);

uint16 大端

const value =
    (data[0] << 8) |
    data[1];

例如:

12 34
->
0x1234

uint16 小端

const value =
    data[0] |
    (data[1] << 8);

例如:

34 12
->
0x1234

取 uint16 高字节

const high =
    (value >> 8) & 0xff;

例如:

0x1234
->
0x12

取 uint16 低字节

const low =
    value & 0xff;

例如:

0x1234
->
0x34

转成 uint32

const unsigned =
    value >>> 0;

例如:

-1
->
4294967295

十八、位运算的阅读思路

看到:

value & 0x80

不要只理解成:

按位与

应该直接翻译成:

检查 / 读取 bit7。


看到:

value | 0x80

应该理解成:

把 bit7 设置为 1。


看到:

value & ~0x80

应该理解成:

把 bit7 清零。


看到:

value ^ 0x80

应该理解成:

翻转 bit7。


看到:

value & 0x0f

应该理解成:

取低 4 位。


看到:

(value >> 4) & 0x03

应该理解成:

读取 bit5~bit4。


看到:

(type & 0x03) << 4

应该理解成:

TYPE 是一个 2 bit 数据,把它放到 bit5~bit4。


看到:

(data[0] << 8) | data[1]

应该理解成:

两个 byte 拼成一个 16 位整数,第一个 byte 是高位。


十九、HID / 通信协议中最常见的思维

通信协议中的一个 byte 经常不是一个普通数字,而是:

bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0

不同 bit 表示不同含义。

例如:

bit7      FIRST
bit6      LAST
bit5~bit4 TYPE
bit3~bit0 SEQ

一个 byte:

10110111

实际上应该拆成:

1 | 0 | 11 | 0111

也就是:

FIRST = true
LAST  = false
TYPE  = 3
SEQ   = 7

代码:

const value = 0b10110111;

const first =
    (value & 0x80) !== 0;

const last =
    (value & 0x40) !== 0;

const type =
    (value >> 4) & 0x03;

const seq =
    value & 0x0f;

反过来组装:

const value =
    (first ? 0x80 : 0) |
    (last ? 0x40 : 0) |
    ((type & 0x03) << 4) |
    (seq & 0x0f);

所以通信协议里的位运算,本质上主要就是两件事:

byte -> 各个字段

组
各个字段 -> byte

这些知识点其实已经覆盖了 HID / WebHID 开发里最常见的 80% 位运算场景

其中最需要真正吃透的是这四组对应关系:

读取单 bit:
value & mask

读取一段 bit:
(value >> offset) & mask

字段写入 byte:
(value & mask) << offset

多个字段组合:
fieldA | fieldB | fieldC