zhangdizhangdi

位运算

进制转换

十进制转二进制

分为整数部分小数部分

整数部分

方法:除2取余,逆序排列,即每次将整数部分除以 2,余数为该位权上的数,而商继续除以 2,余数又为上一个位权上的数,这个步骤一直持续下去,直到商为 0 为止,最后读数时候,从最后一个余数读起,一直到最前面的一个余数。

12 -> 1100

12 除 2, ----- 余 0
6 除 2, ----- 余 0
3 除 2, ----- 余 1
1 除 2, ----- 余 1

小数部分

方法:乘2取整,顺序排列,即将小数部分乘以 2,然后取整数部分,剩下的小数部分继续乘以 2,然后取整数部分,剩下的小数部分又乘以 2,一直取到小数部分

0.125 -> 0.001

0.125 乘 2 = 0.25 ----- 0
0.25 乘 2 = 0.5 ----- 0
0.5 乘 2 = 1 ----- 1

12.125 -> 1100.001

二进制转十进制

方法:按权相加法,即将二进制每位上的数乘以权,然后相加之和即是十进制数。

10101000.001 -> 168.125

1x27 + 0x26 + 1x25 + 0x24 + 1x23 + 0x22 + 0x21 + 0x20 + 0x2-1 + 0x2-2 + 1x2-3

0.1 + 0.2 !== 0.3

0.1 -> 0.0001100110011001100110011001100110011001100110011001101
0.2 -> 0.001100110011001100110011001100110011001100110011001101

位运算

按位运算(&)

两个位都为 1 时,结果才为 1,否则为 0

1&1 为 1,0&0 为 0,1&0 也为 0

0101 & 0011 => 0001 (即 5 & 3 = 1)

按位运算(|)

有一个为 1 时,结果就为 1,两个都为 0 时结果才为 0

1|1 为 1,0|0 为 0,1|0 为 1

0101 | 0011 => 0111 (即 5 | 3 = 7)

按位异或运算(^)

不同时,结果为 1,相同时结果为 0

0^1 为 1,0^0 为 0,1^1 为 0‘

0101 ^ 0011 => 0110 (即 5 ^ 3 = 6)

取反运算(~)

~1 为 0,~0 为 1

~x 等同于 -(x + 1)。例如 ~-1 = 0

左移运算(<<)

全部左移若干位,高位丢弃,低位补 0

常用场景: 乘以 2 的 n 次方

右移运算(>>)

全部右移若干位,低位丢弃,高位补 0 或 1。如果数据的最高位是 0,那么就补 0;如果最高位是 1,那么就补 1。

常用场景: 除以 2 的 n 次方

无符号右行移(>>>)

忽略符号位,空位都以 0 补齐

位运算在前端的应用

1. 权限与状态管理

在很多系统和现代前端框架(如 Vue 3 的 ShapeFlags,React 的 Lane 模型)中,经常用二进制位来表示不同的状态或权限。这种方式极其节省内存,且运算速度极快。

js
// 定义权限 (1, 2, 4, 8)
const READ = 1 << 0;    // 0001 (1)
const WRITE = 1 << 1;   // 0010 (2)
const EXECUTE = 1 << 2; // 0100 (4)
const DELETE = 1 << 3;  // 1000 (8)

// 1. 赋予权限 (按位或 |)
let userRole = READ | WRITE; // 0011 (拥有读和写权限)

// 2. 校验权限 (按位与 &)
// 只要结果还是原来那个权限的值,就说明拥有该权限
const hasWrite = (userRole & WRITE) === WRITE; // true
const hasDelete = (userRole & DELETE) === DELETE; // false

// 3. 删除权限 (按位与 & + 按位非 ~)
userRole = userRole & ~WRITE; // 0001 (去掉了写权限,只剩读)

// 4. 切换权限 (按位异或 ^)
userRole = userRole ^ EXECUTE; // 之前没有执行权限,现在加上了

2. 快速取整(代替 Math.floor / Math.trunc)

位运算会将操作数转换为 32 位整数,利用这个副作用,可以直接去掉浮点数的小数部分。比调用 Math 对象的方法更快。

javascript
const num = 3.14159;

// 以下三种写法都可以快速向下取整(或截断小数)
const a = num | 0;   // 3
const b = ~~num;     // 3 (双重按位非,前端源码里非常常见)
const c = num >> 0;  // 3

// 注意:对负数的处理等同于 Math.trunc,而非 Math.floor
~~(-3.14); // -3
Math.floor(-3.14); // -4

(注意:仅适用于 32 位带符号整数范围,即 $-2147483648$ 到 $2147483647$。超过这个范围会失真!)

3. 奇偶数判断

使用 & 1 取出二进制的最低位。最低位是 1 则是奇数,是 0 则是偶数。性能略好于 % 2

javascript
const isOdd = (num) => (num & 1) === 1;
const isEven = (num) => (num & 1) === 0;

console.log(isOdd(5)); // true

4. 判断索引是否存在(经典的 ~ 操作)

在 ES6 的 Array.prototype.includes 出现之前,前端判断字符串或数组包含某个元素常借助 indexOf,而 indexOf 找不到时返回 -1
因为 ~(-1) 的结果是 0(在 JS 中作为布尔值是 falsy),所以可以这样写:

javascript
const str = "hello world";

// 老写法
if (str.indexOf("world") !== -1) { ... }

// 装逼/极客写法
if (~str.indexOf("world")) { 
    // 如果包含 "world",indexOf 就会返回非 -1 的数,按位非之后就不是 0,进入 if
}

5. RGB 与 Hex 颜色的互转 (Canvas/图形处理)

在 Canvas 像素级操作或者 WebGL 中,颜色计算非常依赖位运算。

RGB 转 16 进制颜色:

javascript
function rgbToHex(r, g, b) {
    // 1 << 24 是为了保证结果是 6 位数 (避免 0 的情况被截断)
    return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
}
rgbToHex(255, 128, 0); // "#ff8000"

16 进制转 RGB:

javascript
function hexToRgb(hex) {
    // 去掉 '#' 并转为 10 进制整数
    const color = parseInt(hex.replace('#', ''), 16);
    const r = (color >> 16) & 255; // 右移 16 位取高八位
    const g = (color >> 8) & 255;  // 右移 8 位取中八位
    const b = color & 255;         // 直接取低八位
    return `rgb(${r}, ${g}, ${b})`;
}

6. 两个变量交换值(不借助第三个变量)

使用异或运算的特性:a ^ a = 0a ^ 0 = a

javascript
let a = 5; // 0101
let b = 3; // 0011

a = a ^ b; 
b = a ^ b; 
a = a ^ b; 
// 此时 a = 3, b = 5

(虽然解构赋值 [a, b] =[b, a] 在现代 JS 中更可读,但异或交换是经典的计算机底层操作。)

总结与面试建议

  • 优点:执行速度极快,在处理组合状态/标志位时非常节省内存(一个变量就可以存 32 种布尔状态)。
  • 缺点:可读性差(容易出现魔法数字),如果是业务代码,必须配上完善的注释;存在 32 位整数上限陷阱。
  • 面试如何应对
    1. 面试官问到时,先讲 Vue/React 源码中的权限/状态标志位 (Flags) 的应用,最能体现你的技术深度。
    2. 如果让你写防抖节流或者某些算法,顺手用个 ~~ 或者 | 0 来做数学取整,能侧面秀一下基本功。
    3. 不要在普通业务代码里滥用,代码的可维护性往往比压榨那微秒级的性能更重要。