JavaScript 位运算:从入门到大厂实战,看这一篇就够了

3 阅读8分钟

很多前端开发者在日常开发中,可能很少直接写位运算代码。大家觉得 + - * / 已经能解决 99% 的问题,位运算看起来又难懂又不直观。但是在很多优秀的开源库(比如 React 的位掩码权限系统、Vue 的静态类型标记)或者高性能算法里,位运算的身影无处不在。

掌握位运算不仅能帮你写出更优雅、性能更高的代码,也是进阶高级工程师、应对大厂面试的必修课。今天我们就把 JavaScript 位运算彻底聊透。


什么是位运算

简单说,位运算就是直接对数字在内存中的二进制位执行操作。

在 JavaScript 里,数字默认是 64 位双精度浮点数。但是位运算不能直接在浮点数上跑,当你执行位运算时,JavaScript 引擎会偷偷把数字转换成 32 位有符号整数

32 位有符号整数是什么样子

  • 它由 32 个 0 或 1 组成
  • 最高位(左边第一位)是符号位:0 代表正数,1 代表负数
  • 正数直接用二进制表示,负数使用二进制补码表示
  • 能够表示的范围是 -2^312^31 - 1(即 -21474836482147483647

比如数字 5 的 32 位二进制是:

00000000 00000000 00000000 00000101

当你明白数字在底层是这样排布的时候,操作它们就变得像拨动开关一样简单。


七大位运算符全解析

JavaScript 一共提供了 7 个位运算符,我们通过代码来逐一拆解。

1. &(按位与 AND)

只有两个对应的二进制位都为 1 时,结果位才为 1。

// 5: 0000...0101
// 3: 0000...0011
//    ----------
//    0000...0001  →  1
console.log(5 & 3); // 1

2. |(按位或 OR)

只要两个对应的二进制位有一个为 1,结果位就是 1。

// 5: 0000...0101
// 3: 0000...0011
//    ----------
//    0000...0111  →  7
console.log(5 | 3); // 7

3. ^(按位异或 XOR)

两个对应的二进制位不相同时,结果位为 1;相同时为 0。

// 5: 0000...0101
// 3: 0000...0011
//    ----------
//    0000...0110  →  6
console.log(5 ^ 3); // 6

异或有一个特别好用的特性:任何数和自身异或结果为 0a ^ a = 0任何数和 0 异或结果为自身a ^ 0 = a

4. ~(按位非 NOT)

把所有的位取反(0 变 1,1 变 0)。在数值上,~x 等价于 -(x + 1)

// 5: 0000...0101
// ~: 1111...1010  →  -6(补码)
console.log(~5);  // -6
console.log(~-1); // 0

5. <<(左移)

把数字的所有位向左移动指定的位数,右边补 0。左移 n 位相当于乘以 2ⁿ

// 5: 0000...0101
// << 2
//    0000...10100  →  20
console.log(5 << 2); // 20(相当于 5 * 4)
console.log(1 << 3); // 8(相当于 2^3)

6. >>(有符号右移)

把数字的所有位向右移动,左边补上符号位(正数补 0,负数补 1)。相当于除以 2ⁿ 后向下取整。

// 20: 0000...10100
// >> 2
//     0000...00101  →  5
console.log(20 >> 2); // 5(相当于 Math.floor(20 / 4))
console.log(-5 >> 1); // -3(负数依然保留符号位)

7. >>>(无符号右移)

向右移动,左边一律补 0(无视符号位)。对正数和 >> 没有区别;对负数,结果会变成一个巨大的正数。

console.log(20 >>> 2); // 5
console.log(-5 >>> 1); // 2147483645(符号位的 1 也被移走了)

运算符速查表

运算符名称规则示例
&按位与都为 1 才是 15 & 3 → 1
|按位或有一个 1 就是 15 | 3 → 7
^按位异或不同为 15 ^ 3 → 6
~按位非全部取反~5 → -6
<<左移右补 05 << 2 → 20
>>有符号右移左补符号位20 >> 2 → 5
>>>无符号右移左补 020 >>> 2 → 5

实战技巧:9 个常用工程与算法场景

技巧 1:奇偶判断

// 平时:n % 2 === 1
// 位运算:奇数的二进制最后一位一定是 1,偶数一定是 0
const isOdd = (n) => (n & 1) === 1;

console.log(isOdd(10)); // false
console.log(isOdd(7));  // true

技巧 2:快速取整(去掉小数)

const n = 10.8;

console.log(~~n);    // 10
console.log(n | 0);  // 10
console.log(n >> 0); // 10

// ⚠️ 注意:对于负数,~~ 是向 0 取整,Math.floor 是向下取整
console.log(~~-10.8);           // -10(向 0 方向)
console.log(Math.floor(-10.8)); // -11(向下)

技巧 3:不使用临时变量交换两数

利用异或自反性:a ^ b ^ b = a

let a = 5, b = 8;

a ^= b;
b ^= a; // b 变成原始的 a
a ^= b; // a 变成原始的 b

console.log(a, b); // 8 5

技巧 4:判断两数是否异号

// 如果异号,符号位不同,异或后符号位为 1,即结果为负数
const hasOppositeSign = (a, b) => (a ^ b) < 0;

console.log(hasOppositeSign(10, -5)); // true
console.log(hasOppositeSign(10, 5));  // false

技巧 5:求绝对值

function fastAbs(n) {
  const mask = n >> 31; // 正数 mask = 0,负数 mask = -1(全 1)
  // 正数:(n ^ 0) - 0 = n
  // 负数:(n ^ -1) - (-1) = ~n + 1(即取反加一,得到正数)
  return (n ^ mask) - mask;
}

console.log(fastAbs(-500)); // 500
console.log(fastAbs(100));  // 100

技巧 6:判断是否为 2 的幂

如果一个数是 2 的幂,它的二进制里只有一个 1(如 4 = 1008 = 1000)。n & (n - 1) 会清除二进制中最低位的 1,2 的幂操作后结果为 0。

const isPowerOfTwo = (n) => n > 0 && (n & (n - 1)) === 0;

console.log(isPowerOfTwo(16)); // true
console.log(isPowerOfTwo(10)); // false
console.log(isPowerOfTwo(1));  // true

技巧 7:任意位的操作(获取、设置、清除、翻转)

这在状态压缩、嵌入式开发中非常常见:

// 获取第 index 位的值(从 0 开始计数)
const getBit    = (n, index) => (n >> index) & 1;

// 将第 index 位置为 1
const setBit    = (n, index) => n | (1 << index);

// 将第 index 位清零
const clearBit  = (n, index) => n & ~(1 << index);

// 翻转第 index 位(0 变 1,1 变 0)
const toggleBit = (n, index) => n ^ (1 << index);

// 演示
let val = 0b1010; // 二进制 1010,十进制 10
console.log(getBit(val, 1));               // 1
console.log(setBit(val, 2).toString(2));   // "1110"
console.log(clearBit(val, 3).toString(2)); // "10"
console.log(toggleBit(val, 0).toString(2)); // "1011"

技巧 8:位掩码实现权限系统

这是位运算最经典的工程应用,React Fiber 里就有类似的实现:

// 定义权限常量(必须是 2 的幂,保证每个权限占一个独立的位)
const READ    = 1 << 0; // 001 (1)
const WRITE   = 1 << 1; // 010 (2)
const EXECUTE = 1 << 2; // 100 (4)

// 赋权:给用户添加读和写权限
let userAuth = READ | WRITE; // 011

// 校验:是否有写权限
const canWrite = (userAuth & WRITE) !== 0;
console.log('可以写吗?', canWrite); // true

// 校验:是否同时有写和执行权限
const canWriteAndExec = (userAuth & (WRITE | EXECUTE)) === (WRITE | EXECUTE);
console.log('有写和执行权限吗?', canWriteAndExec); // false

// 撤权:去掉写权限
userAuth = userAuth & ~WRITE;
console.log('撤销写权限后:', userAuth.toString(2)); // "1"(只剩 READ)

技巧 9:RGB 颜色值编解码

处理 Canvas 图像数据或颜色运算时非常实用:

/**
 * 将 R、G、B 分量编码为一个 24 位整数
 * 高 8 位存 R,中 8 位存 G,低 8 位存 B
 */
function encodeRGB(r, g, b) {
  return (r << 16) | (g << 8) | b;
}

/**
 * 从 24 位整数中解码出 R、G、B 分量
 * 0xFF 即 11111111,用来截取最低 8 位
 */
function decodeRGB(color) {
  return {
    r: (color >> 16) & 0xFF,
    g: (color >> 8)  & 0xFF,
    b:  color        & 0xFF,
  };
}

const orange = encodeRGB(255, 165, 0);
console.log(orange);            // 16752640
console.log(decodeRGB(orange)); // { r: 255, g: 165, b: 0 }

常见陷阱

位运算虽然强大,但在 JavaScript 中有几个坑一定要避开:

陷阱说明
32 位截断超过 2^31 - 1 的数字会被截断,结果错误
浮点数取整位运算会自动丢弃小数部分,精确计算时要小心
负数补码~-1 === 0,负数的位运算结果往往不直观
BigInt 不兼容普通 Number 和 BigInt 不能混用位运算符
// 超过 32 位的数字会溢出
console.log(2147483647 + 1); // 2147483648(普通加法正确)
console.log(2147483647 | 1); // 2147483647(位运算正常)
console.log(2147483648 | 0); // -2147483648(溢出!变成负数了)

性能说明:位运算真的更快吗?

在 C/汇编时代,位运算毫无疑问比算术运算快得多。但在现代 JavaScript 引擎(V8)中,情况变得复杂了:

  • 引擎自动优化:V8 非常聪明,Math.floor(x / 2) 在底层很可能已经被自动优化成位移操作
  • 类型转换开销:JS 数字是 64 位浮点数,执行位运算前需要转成 32 位整数,算完再转回来,这个来回本身有额外开销

结论:不要为了微乎其微的性能提升把所有代码改写成位运算,可读性永远是第一位的

但以下场景位运算依然是首选:

  1. 海量状态压缩:32 个开关状态用 1 个数字存,比长度为 32 的数组省空间、省遍历时间
  2. 特定算法需求:哈希函数、加密算法、图像像素处理
  3. 权限控制标记:代码简洁,逻辑清晰,天然防止无效组合

总结

位运算不是什么高深莫测的黑魔法,它本质上就是一套直接操作二进制位的工具。

本文学到的核心内容:

  • 位运算在 JS 中基于 32 位有符号整数运行
  • 掌握了 &|^~<<>>>>> 7 个运算符
  • 学会了奇偶判断、快速取整、交换变量、权限控制、颜色编解码等 9 个实战场景
  • 了解了 32 位边界、浮点数截断等常见陷阱

下次当你看到别人代码里出现 ~~ 或者 n & (n - 1) 时,希望你不再感到困惑