从进制到实战的按位与使用

146 阅读8分钟

你是否好奇按位与运算的妙用?想了解进制转换背后的逻辑吗?本文将带你从基础入手,逐步揭开按位与的 "超能力"(嘿,醒醒,吃饭啦)。

一、基础认知:进制与按位与

(一)进制转换:数字的不同 "表达方式"

在了解按位与之前,我们先掌握进制转换的核心逻辑。日常使用的十进制数字可以转换为二进制、八进制等其他进制,number.toString(base) 方法的底层逻辑可简化为以下实现:

function decimalToBase(decimal: number, base = 2): string {
  if (base < 2 || base > 36) {
    throw new Error('进制必须在2到36之间');
  }
  if (decimal === 0) {
    return '0'; // 0的任何进制表示都是"0"
  }

  let num = decimal;
  let result = '';
  const digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; // 进制字符集

  while (num > 0) {
    const remainder = num % base; // 取余数作为当前位
    result = digits[remainder] + result; // 拼接结果(从低位到高位)
    num = Math.floor(num / base); // 去除已处理的低位
  }
  return result;
}

核心逻辑:通过 "除基取余,逆序排列" 的方式,将十进制数字转换为目标进制(2-36 进制),结果由字符集 digits 中对应位置的字符组成。

(二)按位与:二进制层面的 "交集运算"

按位与(&)是针对二进制的位运算,规则为:两个数字转换为二进制后,对应位均为 1 则结果位为 1,否则为 0,最终将结果转回十进制。

  示例:十进制数字的二进制表示与按位与计算

先明确几个数字的二进制形式(0b 表示二进制前缀):

1 = 0b00000001  
2 = 0b00000010  
3 = 0b00000011  
4 = 0b00000100  
5 = 0b00000101  
6 = 0b00000110  
7 = 0b00000111  
8 = 0b00001000  
...  
16 = 0b00010000  

再看按位与计算过程:

1 & 2 → 0b00000001 & 0b00000010 → 0b00000000 → 0  
1 & 3 → 0b00000001 & 0b00000011 → 0b00000001 → 1  
2 & 3 → 0b00000010 & 0b00000011 → 0b00000010 → 2  
4 & 3 → 0b00000100 & 0b00000011 → 0b00000000 → 0  

转换工具:二进制转十进制可用 parseInt(二进制字符串, 2),例如 parseInt('0b00000010', 2)===2

(三)按位或:二进制层面的"或运算"

按位或(|)核心逻辑为转换二进制后,对应位置有1则为1,否则为0,即0|0=0,1|1=1,1|0=1,0|1=1

1 | 2 → 0b00000001 | 0b00000010 → 0b00000011 → 3  
1 | 3 → 0b00000001 | 0b00000011 → 0b000000011 → 3  
2 | 3 → 0b00000010 | 0b00000011 → 0b00000011 → 3  
4 | 3 → 0b00000100 & 0b00000011 → 0b00000111 → 7  

注意:我们可以注意到,在按位或的逻辑里,所有含1的进至计算都会为1,所以,按位或后的数字,在按位与计算里,几个数字按位或后的结果,一定可以按位与这个结果等于本身

二、进阶:进制与按位与的核心规律

(一)二进制的关键特性

二进制作为计算机底层的 "语言",有几个核心规律需牢记:

  1. 2ⁿ 的二进制形式:永远是 0b1 后接 n0(如 2³=8 → 0b1000)。
  2. 2ⁿ - 1 的二进制形式:永远是 n 个连续的 1(如 2³-1=7 → 0b111)。
  3. 二进制加法:遵循 "逢二进一",例如 0b1 + 0b1 = 0b10(1+1=2)、0b11 + 0b1 = 0b100(3+1=4)。

(二)按位与的运算规律

基于二进制特性,按位与有几个高频使用的规律:

  1. 2ⁿ - 1 与 2ᵐ(m < n)的按位与:结果为 2ᵐ 例:7(0b00000111,即 2³-1)& 2(0b00000010,即 2¹)= 2
  2. 2ⁿ 与 2ᵐ(m ≠ n)的按位与:结果为 0;仅当 m = n 时,结果为自身 例:4(0b00000100)& 8(0b00001000)= 0;4 & 4 = 4
  3. 多个不同 2ⁿ 之和的按位与:若某数是总和的组成部分,则按位与结果为该数,否则为 0 例:5(0b00000101 = 4+1)& 4 = 4;5 & 2 = 0
  4. 多个不同 2ⁿ 之和的按位或:一定等于多数之和 例:1 | 4 = 0b00000001 | 0b00000100 = 0b00000101 = 5
  5. 多个数字的按位或:多个数字按位或后的结果,将可以按位与每个被按位或的数字,但是2ⁿ永远只能按位与自己本身,2ⁿ-1永远可以按位与比自己小的任何正数。

三、按位与的优势与实战场景

(一)为什么用按位与?

  1. 性能优势:基于二进制的位运算直接在 CPU 层面执行,比循环、数组查找(如 include)更快。
使用include使用按位与
  1. 简洁高效:用一个数字即可表示多个状态的组合(如权限集合),减少存储和传输成本。
// 使用include
const permissionMap={
A:"PERMISSION_A",
B:"PERMISSION_B",
C:"PERMISSION_C"
}
const permissionA = [permissionMap.A,permissionMap.B];
const permissionB = [permissionMap.C,permission.B,permission.A];
const permissionList = [permissionA,permissionB][Math.round(Math.random())];
const showA = permissionList.include(permissionMap.A);
const showB = permissionList.include(permissionMap.B);
const showC = permissionList.include(permissionMap.C);
//使用按位与
const permissionA = 1;
const permissionB = 2;
const permissionC = 4;
const permissionsShowA_B = 1|2;
const permissionsShowA_B_C = 1|2|4;
const permission = [permissionA,permissionB][Math.round(Math.random())];
const showA = permission&permissionA === permissionA ;
const showB = permission&permissionB === permissionB ;
const showC = permission&permissionC === permissionC ;

3. 内存优势 : 进至计算占内存字节更少。

为了可观性创建的字符串长一些造成堆存储,但也可以看出保留大小加上指针指向后有着明显的内存差异

(二)实战案例:权限管理系统

需求

有 4 个标签页(tab1-tab4),权限分别对应 1、2、4、8(均为 2ⁿ 形式)。不同用户的权限如下:

  • a 同学:能看所有 tab(1、2、4、8)
  • b 同学:能看 tab1、tab3(1、4)
  • c 同学:能看 tab1、tab2(1、2)
  • d 同学:只能看 tab4(8)

传统方案 vs 按位与方案

  • 传统方案:用数组存储用户权限(如 [1,2,4,8]),判断时需用 include 检查是否包含目标权限,效率较低。
  • 按位与方案:用一个数字表示权限组合(各权限之和),通过 权限 & 目标值 === 目标值 判断是否拥有权限。

代码实现

// 定义标签页及其对应权限(均为2ⁿ)
const tabs = [
  { title: "tab1", permission: 1 },   // 2⁰
  { title: "tab2", permission: 2 },   // 2¹
  { title: "tab3", permission: 4 },   // 2²
  { title: "tab4", permission: 8 }    // 2³
];

// 根据用户权限计算可展示的标签页
function getVisibleTabs(userPermission) {
  return tabs.filter(tab => (tab.permission & userPermission) === tab.permission);
}

// 不同用户的权限(各标签权限按位或后得到的结果)
const userPermissions = {
  a: 1 | 2 | 4 | 8 = 15,  // 0b00001111
  b: 1 | 4 = 5,           // 0b00000101
  c: 1 | 2 = 3,           // 0b00000011
  d: 8                    // 0b00001000
};

// 示例:获取b同学可看的标签页
console.log(getVisibleTabs(userPermissions.b)); // 输出:[{title: "tab1"}, {title: "tab3"}]

核心逻辑:用户权限是其可访问标签权限的总和(因每个权限是独立的 2ⁿ,总和的二进制中每个 1 对应一个权限)。通过按位与可快速判断 "该权限是否在用户权限集合中"。

四、拓展

Q: 为什么有些场景不使用按位与,按位与有什么缺陷

A:

  1. 受二进制位的限制,用32位整数只能有32个权限,64位整数只能有64个权限,而且还需要考虑Number.MAX_SAFE_INTEGER
  2. 可读性较低,需要开发人员足够了解权限对应的点
  3. 对于较复杂的逻辑(多权限判别、权限有依赖或者互斥场面)的判别会较为复杂
  4. 在各类语言中的处理会有些许差异

总结

按位与运算借助二进制的特性,在性能和简洁性上展现了独特优势,尤其适合权限管理、状态组合等场景。掌握其底层逻辑后,你会发现更多如数据压缩、位掩码等高级用法 —— 这正是二进制世界的魅力所在。