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 mask:
00001000
执行:
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