Skip to content

JavaScript 位运算详解

位运算的本质就是“直接和计算机对话”——计算机只认识 0 和 1(二进制),就像我们只认识汉字、英文一样,位运算就是跳过“十进制转二进制”的中间步骤,直接操作这些 0 和 1,即快又省内存,一点都不复杂。

本文将会从底层基础6 种位运算,再到实战案例,一步一步带你吃透位运算的原理。

彻底搞懂二进制

位运算的所有操作,都基于「二进制」——计算机的“母语”。本章节先把二进制是什么、怎么来的、计算机如何存储、正负数如何区分这些问题全部讲解清楚,后面学习位运算就会水到渠成,不用死记硬背,理解了底层,规则自然就会了。

十进制和二进制的区别

我们平时计数、花钱、算账,使用的都是「十进制」,原因很简单——我们有 10 根手指,数到 10 就没法再数了,只能进 1 位,这就是“逢 10 进 1”。

而计算机没有“手指”,它的核心是晶体管,晶体管只有“通电”和“断电”两种状态,正好对应 0(断电)和 1(通电)。所以计算机只能用二进制计数,数到 2 就进 1 位,这就是“逢 2 进 1”。

十进制二进制解释
00没有就是 0,和十进制一样
11只有 1 个,记位 1,也和十进制一样
210二进制逢 2 进 1,对应“1 个 2,和 0 个 1”,所以是 10
311在 2(10)的基础上再加 1 个 1,就是 “1 个 2,和 1 个 1”,所以是 11
4100再逢 2 进 1,对应“1 个 4,0 个 2,和 0 个 1”,所以是 100
51014(100)加 1,就是“1 个 4,0 个 2,和 1 个 1”,所以是 101
61104(100)加 2,就是“1 个 4,1 个 2,和 0 个 1”,所以是 110
71114(100)加 2(10)加 1(1),三个位都是 1,所以是 111
81000再逢 2 进 1,对应“1 个 8,0 个 4,0 个 2,和 0 个 1”,所以是 1000

记住两个核心:

  1. 二进制的每一位,都叫「比特位(bit)」,从右往左数,第 1 位是 20(也就是 1),第 2 位是 21(也就是 2),第 3 位是 22(也就是 4),第 4 位是 23(也就是 8)……以此类推(右边是低位,左边是高位,位数越往左,数值越大)。

  2. 二进制转十进制,直接“拆位相加”即可。比如二进制 101,拆解就是:

    1×22+0×21+1×20=4+0+1=5

    再比如二进制 110,拆解就是:

    1×22+1×21+0×20=4+2+0=6

    反过来,十进制转二进制也不用复杂计算,记住“除以 2 取余数,倒着排”。比如十进制 6,除以 2 得 3 余 0,得数 3 再除以 2 得 1 余 1,得数 1 再除以 2 得 0 余 1,得数为 0 时停止计算,最后把所有的余数倒着排就是二进制 110。

计算机如何存储二进制

JavaScript 中的位运算,不管你输入的是整数、小数或者负数,都会自动先把这个数字转换成「32 位有符号整数」,再进行运算,运算结束后,再转成十进制返回给我们。

什么是 32 位有符号整数?就是计算机用 32 个位置(32 个比特位)来存储一个整数。这 32 个位置分成两部分,各司其职:

  • 总共 32 个位置(比特位),从左到右编号 1-32(第 1 位是最左边,第 32 位是最右边);
  • 第 1 位(最左边)是「符号位」,专门用来区分正数和负数——0 表示正数,1 表示负数;
  • 剩下的 31 位(第 2 位到第 32 位)用来存储数字的绝对值(也就是数字本身的二进制形式),这部分叫「数值位」。

举个超直观的例子:十进制 5(正数),32 位二进制存储形式是这样的(为了方便观看,用空格分为 4 组,每组 8 位,实际计算机存储是连续的 32 个 0 或 1):

00000000 00000000 00000000 00000101

拆解一下:第 1 位是 0(表示正数),后面 31 位是 0000000 00000000 00000000 00000101,这样计算机一看就知道,这个数是正数 5。

提示

为什么是 32 位?不是 64 位、16 位?因为 JavaScript 最初设计时,借鉴了其他语言的规范,位运算统一使用 32 位整数,这样计算速度最快,也能满足大部分日常开发的需求(32 位整数能表示的范围是:2312311,也就是 -2147483648 到 2147483647,足够我们日常使用了)。

负数的二进制如何存储

正数的二进制很简单,直接转成二进制,再补够 31 位数值位,加上符号位 0,就是计算机的存储形式。但负数的二进制,计算机不会直接存“负号 + 绝对值”(比如 -5,不会存 -101),而是用「补码」存储——这也是为什么很多人算“按位非”或“负数位运算”会懵的核心原因,搞懂补码,负数运算就再也不怕了。

先搞清楚一个小问题:为什么计算机不用“负号 + 绝对值”存负数?因为计算机只有加法器,没有减法器,用补码可以把“减法”变成“加法”,计算更简单(比如 53,相当于 5+(3),计算机只需要算加法就行),这是补码的核心作用。

补码的计算规则分 3 步:

  1. 先求这个负数的「绝对值」,然后把绝对值转成二进制;
  2. 把这个二进制补成 31 位(符号位占 1 位,数值位只有 31 位),不足 31 位的,在前面补 0;
  3. 对这 31 位数值位「按位取反」(0 变 1,1 变 0),然后加 1,得到“补码的数值部分”,最后把符号位设为 1(表示负数),就是这个负数的 32 位补码。

下面用一个例子来说明补码的详细步骤,比如十进制 -5:

  1. 求 -5 的绝对值得到 5,5 的二进制是 101;
  2. 补成 31 位数值位得到 0000000 00000000 00000000 00000101
  3. 按位取反得到 1111111 11111111 11111111 11111010
  4. 取反后加 1 得到 1111111 11111111 11111111 11111011
  5. 最后加上符号位 1(最左边)得到 11111111 11111111 11111111 11111011,这就是 -5 的 32 位二进制存储形式,也就是补码。

注意

  1. 计算机存储负数,只存补码,不存“负号 + 绝对值”。所有位运算(包括按位非、按位与等)都是基于补码进行的——后面学“按位非”时,我们就会明白,为什么 ~5 = -6,本质就是补码取反加 1 的过程。
  2. 补码的“取反加 1”是可逆的。比如知道 -5 的补码,想求它的绝对值,只要对补码的数值部分再取反加 1,就能得到 5 的二进制(11111111 11111111 11111111 11111011 取反得到 00000000 00000000 00000000 00000100,加 1 就是 00000000 00000000 00000000 00000101,也就是 5 的二进制)。
  3. 用公式概括补码的特性就是:-n = ~n + 1 或者 n = ~(-n) + 1

JavaScript 中查看二进制

JavaScript 中不用手动换算二进制、补码,用 2 个简单方法,就能快速查看数字的二进制形式,方便调试、验证位运算结果。

js
// 1. 十进制转二进制字符串(简单直观,适合正数)
(5).toString(2); // 输出 101(正数,直接显示二进制,省略前面的 0)
(-5).toString(2); // 输出 -101(这里显示的是简化版,不是计算机实际存储的补码)

// 2. 查看 32 位补码(最精准,适合调试负数、位运算)
function to32BitBinary(n) {
  // 利用“无符号右移 0 位”,强制把数字转换成 32 位无符号整数,再转二进制
  // padStart(32, '0') 是为了补够 32 位,避免前面的 0 被省略
  return (n >>> 0).toString(2).padStart(32, '0');
}
console.log(to32BitBinary(5)); // 输出 00000000000000000000000000000101(5 的 32 位存储)
console.log(to32BitBinary(-5)); // 输出 11111111111111111111111111111011(-5 的 32 位补码)

提示

>>> 是“无符号右移”(后面会详细讲),这里用 n >>> 0,核心作用是“强制把数字转换成 32 位无符号整数”——这样就能看到计算机实际存储的 32 位二进制(包括符号位和补码),不会像 toString(2) 那样简化显示,调试位运算时特别好用。

6 种位运算

JavaScript 中常用的位运算有 6 种,全部基于“32 位有符号整数”的二进制位进行操作。

注意

所有位运算执行前,JavaScript 会自动把数字转成 32 位有符号整数(如果是小数,会直接去掉小数部分,只保留整数;如果是负数,会转成补码)。运算结束后,再转成十进制返回。

按位与 &

  • 规则

    两个数字的 32 位二进制,对应每一位(比如第一个数的第 32 位和第二个数的第 32 位,第一个数的第 31 位和第二个数的第 31 位)进行比较。只有当两个位都是 1 时,结果对应的位才是 1;只要有一个是 0,结果位就是 0。

    就像“同时满足两个条件才能做某事”——比如去银行取大额现金,只有银行卡和身份证同时都有才行,只有银行卡不能取大额现金,只有身份证也不行,两者都没有自然更不行了。

    换句话说就是全真为真

  • 示例:计算 5 & 3

    1. 把 5 和 3 都转成 32 位二进制(为了方便看,这里只使用最后 4 位,前面全是 0,不影响结果):

      5 → 0101

      3 → 0011

    2. 对应每一位进行“按位与”运算(从右往左,逐位比较):

      5 的第 4 位(最右边)是 1,3 的第 4 位也是 1,则 1 & 1 = 1;

      5 的第 3 位是 0,3 的第 3 位是 1,则 0 & 1 = 0;

      5 的第 2 位是 1,3 的第 2 位是 0,则 1 & 0 = 0;

      5 的第 1 位(最左边,符号位)是 0,3 的第 1 位也是 0,则 0 & 0 = 0;

    3. 把运算后的每一位组合起来,得到二进制结果 0001(完整 32 位前面全是 0);

    4. 把二进制结果转成十进制:0001 → 1,所以 5 & 3 = 1

    补充

    很多人算负数的按位与会懵,其实只要记住“先转补码,再逐位运算,最后转十进制”,一步都不跳,就能算对。我们以计算 -5 & 3 为例,拆解每一步:

    1. 把两个数都转成 32 位二进制(-5 转补码,3 转正数二进制):

      -5 的补码:11111111 11111111 11111111 11111011(最后 4 位 1011)

      3 的 32 位二进制:00000000 00000000 00000000 00000011(最后 4 位 0011)

    2. 对应每一位进行按位与运算(逐位比较,符号位也参与运算):

      -5 的第 4 位是 1,3 的第 4 位是 1,则 1 & 1 = 1;

      -5 的第 3 位是 1,3 的第 3 位是 1,则 1 & 1 = 1;

      -5 的第 2 位是 0,3 的第 2 位是 0,则 0 & 0 = 0;

      -5 的第 1 位(符号位)是 1,3 的第 1 位是 0,则 1 & 0 = 0;

    3. 把运算后的每一位组合起来,得到二进制结果 0011(完整 32 位前面全是 0);

    4. 把二进制结果转成十进制:0011 → 3,所以 -5 & 3 = 3

    负数按位与的核心,就是“先转补码,再运算”,运算后如果结果的符号位是 0,就是正数;如果是 1,就把结果的补码转成十进制(取反加 1 求绝对值,再加负号)。

    比如 -5 & -3,-5 的二进制补码(最后 4 位)为 1011,-3 的二进制补码(最后 4 位)为 1101,按位与后得到 1001,符号位是 1,说明结果是负数,把 1001 取反加 1 得到 0111,转成十进制是 7,最后加上负号,所以 -5 & -3 = -7

  • 用途

    1. 判断奇偶(最常用):n & 1,如果结果是 1,就是奇数;结果是 0,就是偶数;

    2. 将二进制某一位设为 0:比如想把一个数的第 4 位(从左往右数,也就是 20=1 的位置)设为 0,就用 n & ~1~1 是全 1 除了第 4 位是 0,按位与后,第 4 位就会变成 0。

按位或 |

  • 规则

    两个数字的 32 位二进制,对应每一位进行比较。只要有一个位是 1,结果位就是 1;只有两个位都是 0,结果位才是 0。

    就像“满足一个条件就能做某事”——比如去景区买票,学生证或身份证只要有一个就能买票,如果两个都没有就买不了。

    换句话说就是有真为真

  • 示例 1:计算 5 | 3

    1. 5 的 32 位二进制(最后 4 位)为 0101;3 的 32 位二进制(最后 4 位)为 0011;

    2. 逐位按位或运算:1 | 1 = 10 | 1 = 11 | 0 = 10 | 0 = 0,二进制结果为 0111;

    3. 转十进制:0111 → 7,所以 5 | 3 = 7

  • 示例 2:计算 -5 | 3

    1. -5 的 32 位补码(最后 4 位)为 1011;3 的 32 位二进制(最后 4 位)为 0011;

    2. 逐位按位或运算:1 | 1 = 11 | 1 = 10 | 0 = 01 | 0 = 1,二进制结果为 1011;

    3. 结果符号位是 1(负数),转补码(取反加 1)得到 0101,转十进制是 5,加上负号,所以 -5 | 3 = -5

  • 用途

    1. 快速取整(比 Math.floor 快,常用):任意数字 | 0,就能去掉小数部分,比如 5.99 | 0 = 5-2.8 | 0 = -2

    2. 权限组合(经典用途):用不同的二进制表示不同权限,按位或可以合并多个权限(比如读权限 1 + 写权限 2,1 | 2 = 3,3 就表示同时有读和写的权限);

    3. 将二进制某一位设为 1:比如想把一个数的第 4 位(20=1 的位置)设为 1,就用 n | 11 的二进制是 0001,按位或后,第 4 位就会变成 1,其他位不变。

按位异或 ^

  • 规则

    两个数字的 32 位二进制,对应每一位进行比较。两个位不一样(一个 0、一个 1),结果位就是 1;两个位一样(都是 0 或都是 1),结果位就是 0。

    就像“两个人不一样才可以组队”——甲乙必须是一男一女才能组队,甲乙都是男或都是女则不能组队。

    换句话说就是不同为真

    按位异或有 3 个非常实用的特性:

    1. 自己异或自己 = 0a ^ a = 0)——比如 5 ^ 5,转为二进制就是 0101 ^ 0101,每一位都一样,结果都是 0,所以 5 ^ 5 = 0

    2. 异或 0 = 自己a ^ 0 = a)——比如 5 ^ 0,转为二进制就是 0101 ^ 0000,每一位都和 0 对比,不一样的就是自己,结果还是 0101,所以 5 ^ 0 = 5

    3. 可交换、可结合a ^ b ^ c = a ^ c ^ b = (a ^ b) ^ c)——这个特性是“不用临时变量交换两个数”的核心。

  • 示例 1:计算 5 ^ 3

    1. 5 → 0101,3 → 0011;

    2. 逐位异或运算:1 ^ 1 = 00 ^ 1 = 11 ^ 0 = 10 ^ 0 = 0,二进制结果为 0110;

    3. 转十进制:0110 → 6,所以 5 ^ 3 = 6

  • 示例 2:计算 -5 ^ 3

    1. -5 补码 → 1011,3 → 0011;

    2. 逐位异或运算:1 ^ 1 = 01 ^ 1 = 00 ^ 0 = 01 ^ 0 = 1,二进制结果位 1000;

    3. 结果符号位是 1(负数),转十进制(取反加 1)为 1000 → 8,加上负号,所以 -5 ^ 3 = -8

  • 用途

    1. 不用临时变量交换两个数(面试常考);

    2. 反转二进制某一位:比如想反转第 4 位(0 变 1、1 变 0),就用 n ^ 1,异或后,第 4 位会反转,其他位不变。

按位非 ~

  • 规则

    对一个数字的 32 位二进制(或补码),每一位都取反:0 变 1,1 变 0(包括符号位)。

    就像“开关的反向操作”——开关开着,按一下就关了;开关关着,按一下就开了。所有开关都要反向。

    换句话说就是反向操作

    提示

     x=(x+1) 这个公式能快速算出按位非的结果,不用再转二进制、取反、转十进制,节省时间。

  • 示例 1(计算 ~5):

    • 方法 1:用公式(快速计算)

      ~5 = -(5 + 1) = -6

    • 方法 2:用二进制(理解原理)

      5 的 32 位二进制(最后 4 位)为 0101,按位非后变成 1010,结果符号位是 1(负数),转十进制(取反加 1)为 0110 → 6,加上负号,所以 ~5 = -6,和公式结果一致。

  • 示例 2(计算 ~-3):

    • 方法 1:用公式

      ~-3 = -(-3 + 1) = -(-2) = 2

    • 方法 2:用二进制

      -3 的补码(最后 4 位)为 1101,按位取反变成 0010,符号位 0(正数),转十进制为 2,和公式结果一致。

  • 用途

    1. 快速判断数组是否包含某个元素arr.indexOf(元素) 返回索引,找不到返回 -1,~-1 = 0;找到返回 ≥ 0 的索引,~索引 ≠ 0,所以 if (~arr.indexOf(元素)) 就能判断是否包含该元素;

    2. 快速取反(比如把 0 变成 -1、1 变成 -2,本质就是用公式 ~x = -(x + 1));

左移 <<

  • 规则

    把一个数字的 32 位二进制(或补码),整体向左移动 n 位,左边移出的位置直接丢弃,右边空出来的位补 0。

    就像“计数翻倍”——比如有 3 个苹果(二进制 0011),翻倍一次(左移 1 位)变成 6 个(0110),再翻倍一次(左移 1 位)变成 12 个(1100)。和“乘以 2 的 n 次方”完全对应。

    换句话说就是翻倍

    提示

    x<<n=x×2n 这个公式能快速算出左移的结果,不用再转二进制、移动、转十进制,节省时间。

  • 示例 1(计算 3 << 1):

    • 方法 1:用公式

      3<<1=3×21=6

    • 方法 2:用二进制

      3 的 32 位二进制(最后 4 位)为 0011,向左移动 1 位,左边移出的 0 丢弃,右边补 0,结果为 0110,转十进制就是 6,结果与公式一致。

  • 示例 2(计算 -3 << 1):

    • 方法 1:用公式

      3<<1=3×21=6

    • 方法 2:用二进制

      -3 的补码(最后 4 位)为 1101,向左移动 1 位,左边移出的 1 丢弃,右边补 0,结果为 1010,符号位 1(负数),转十进制为 0110 → 6,加上负号为 -6,结果与公式一致。

  • 用途

    1. 快速乘以 2 的 n 次方(比乘法运算快):比如 4 << 2 = 4 x 4 = 16

    2. 快速生成 2 的 n 次方:比如 1 << 3 = 8

    3. 权限设置:比如想设置第 n 位权限(从右数),就用 1 << (n - 1)

右移 >>

  • 规则

    把一个数字的 32 位二进制(或补码),整体向右移动 n 位,右边移出的位置直接丢弃,左边空出来的位补符号位(正数补 0,负数补 1)。

    就像“计数减半”——比如有 8 个苹果(二进制 1000),减半一次(右移 1 位)变成 4 个(0100),再减半一次(右移 1 位)变成 2 个(0010)。和“除以 2 的 n 次方”完全对应。

    换句话说就是减半

    提示

    x>>n=Math.floor(x÷2n) 这个公式能快速算出左移的结果,不用再转二进制、移动、转十进制,节省时间。

    补充

    无符号右移 >>> 和 右移 >> 的区别是,左边空出来的位不管符号位,一律补 0,所以负数无符号右移后,会变成正数。前面用来查看补码的方法就是用了无符号右移。

  • 示例 1:计算 8 >> 1

    8 的 32 位二进制(最后 4 位)位 1000,向右移动 1 位,右边移出的 0 丢弃,左边补 0(正数),结果为 0100,转十进制就是 4,所以 8 >> 1 = 4,和规律 8÷21=4 一致。

  • 示例 2:计算 -8 >> 1

    8 的补码(最后 4 位)位 1000,向右移动 1 位,右边移出的 0 丢弃,左边补 1(负数),结果为 1100,转十进制就是 0100 → 4,加上负号就是 -4,所以 -8 >> 1 = -4,和规律 8÷21=4 一致。

  • 示例 3:计算 8 >>> 1-8 >>> 1

    1. 正数无符号右移:8 >>> 18 >> 1 结果一致,都是 4,因为左边都是补 0;

    2. 负数无符号右移:-8 的补码是 11111111 11111111 11111111 11111000,向右移动 1 位,左边补 0,结果是 01111111 11111111 11111111 11111100,转为十进制为 2147483644(正数,符号位变成 0)。这就是无符号右移和右移的核心区别。

  • 用途

    1. 快速除以 2 的 n 次方(比除法运算快,且自动向下取整):比如 16>>2=16÷4=417>>2=Math.floor(17÷4)=5

    2. 获取数字的一半(向下取整):比如 10 >> 1 = 57 >> 1 = 3-9 >> 1 = -5

实战案例

学完 6 种位运算,你可能还是会问“到底什么时候用”?本章节整理了一些开发中最常用的实战场景,每个场景配有完整的代码和位运算原理说明,彻底解决“学了用不上”的问题。

判断奇偶

  • 原理

    二进制的最后一位(第 32 位),如果是 1 表示奇数,如果是 0 表示偶数。

    因为 1 的 32 位二进制为 0000...0001,只有最后一位是 1,而按位与的规则是全真为真,所以 n & 1 后,n 的其他位都会变成 0,而最后一位要么是 0,要么是 1。

    如果 n & 1 的最后一位是 0,说明 n 的最后一位是 0,那么 n 是偶数;如果 n & 1 的最后一位是 1,说明 n 的最后一位是 1,那么 n 是奇数。

  • 代码

    js
    function isOdd(n) {
      return n & 1; // 1 → 奇数,0 → 偶数
    }
    console.log(isOdd(5)); // 1 → 奇数
    console.log(isOdd(6)); // 0 → 偶数
    console.log(isOdd(-3)); // 1 → 奇数(-3 的补码为 1101,最后一位是 1,奇数)
    
    // 对比传统取余方法(效率低,且负数可能出问题)
    function isOddNormal(n) {
      return n % 2 !== 0;
    }

提示

位运算直接操作二进制,比取余运算(%)更快,且对负数判断更精准,代码更简洁。

快速取整

  • 原理

    首先,任何位运算都会强制将数字转成 32 位有符号整数,自动去掉小数部分。然后,因为按位或的规则是有真为真,所以采用 n | 0 时,会保持 n 不变,从而实现快速取整。

  • 代码

    js
    // 位运算向下取整(仅去掉小数部分,不做任何四舍五入)
    function floor(n) {
      return n | 0;
    }
    console.log(floor(5.99)); // 5
    console.log(floor(-2.8)); // -2
    
    // 对比 Math.floor(效率低 30%+,代码略长,且负数时会有意想不到的效果)
    console.log(Math.floor(5.99)) // 5
    console.log(Math.floor(-2.8)) // -3

注意

仅适用于 32 位整数范围内的数字(-2147483648 ~ 2147483647),超出范围会出现异常。日常开发中,大部分场景都在这个范围内。

交换两个数字

  • 原理

    利用按位异或的 3 个特性(a^a=0、a^0=a、可交换可结合),不用临时变量 temp,就能实现两个数字的交换。

    拆解过程:

    1. a = a ^ b——此时 a 存储了 a 和 b 的差异(因为异或只有不同才是 1,相当于记录了 a 和 b 哪些位不一样);

    2. b = a ^ b——a ^ b 就是原来的 a(因为 a 现在是 a ^ ba ^ b ^ b = a ^ (b ^ b) = a ^ 0 = a);

    3. a = a ^ b——a ^ b 就是原来的 b(因为 a 现在是 a ^ b,而 b 现在是原来的 a,a ^ b ^ a = (a ^ a) ^ b = 0 ^ b = b)。

  • 代码

    js
    // 位运算交换两个数字(无临时变量)
    function swap(a, b) {
      a = a ^ b; // a 存储 a 和 b 的差异(异或结果)
      b = a ^ b; // b = a ^ b = 原 a(a ^ b ^ b = a ^ (b ^ b^) = a ^ 0 = a)
      a = a ^ b; // a = a ^ b = 原 b(a ^ a ^ b = b ^ (a ^ a) = b ^ 0 = b)
      return [a, b];
    }
    let [x, y] = swap(5, 10);
    console.log(x, y); // 10 5(交换成功)
    
    // 对比传统方法(需要临时变量)
    function swapWithTemp(a, b) {
      let temp = a;
      a = b;
      b = temp;
      return [a, b];
    }

提示

使用按位异或的方法交换两个数字,不用额外占用内存(无临时变量),代码更简洁,是面试中“代码优化”的常见考点。

判断某数是否 2 的幂

  • 原理

    2 的幂的二进制有一个核心特征——只有一位是 1,其余全是 0(比如 2 → 10,4 → 100,8 → 1000);

    而 n - 1 的二进制,会把这个唯一的 1 变成 0,后面的所有 0 变成 1(比如 8 - 1 = 7 → 0111);

    由此可以得出一个结论,如果 n 是 2 的幂,那么 n 和 n - 1 的二进制中,0 和 1 是相反的,即 n 中是 1 的位置,n - 1 对应的位置是 0,反之亦然;

    根据这个结论,如果 n 是 2 的幂,那么 n & (n - 1) 的结果一定是 0,因为两者每一位对应的 0 和 1 都相反;

    反之,如果 n 不是 2 的幂,那么 n 的二进制中会有多个 1,n & (n - 1) 的结果就不会是 0(比如 6 → 110,5 → 101,110 & 101 = 100 ≠ 0)。

  • 代码

    js
    function isPowerOfTwo(n) {
      // 注意:n 必须大于 0,因为 0 和负数都不是 2 的幂
      return n > 0 && (n & (n - 1)) === 0
    }
    
    console.log(isPowerOfTwo(4)) // true (4 → 0100, 3 → 0011, 4 & 3 → 0000 = 0)
    console.log(isPowerOfTwo(8)) // true (8 → 1000, 7 → 0111, 8 & 7 → 0000 = 0)
    console.log(isPowerOfTwo(6)) // false (6 → 0110, 5 → 0101, 6 & 5 → 0100 ≠ 0)
    console.log(isPowerOfTwo(1)) // true (1 & 0 = 0)

补充

1 是 2 的 0 次方(20=1),并且 1 的 二进制就是 1,符合“只有一个 1”的特性,所以 isPowerOfTwo(1) 返回 true。这是容易踩坑的地方。

判断数组是否包含某个元素

  • 原理

    首先需要了解数组的 indexOf 方法——找不到元素时返回 -1,找到时返回 ≥0 的索引;

    然后按位非计算 indexOf 的返回值——按位非的公式是 ~x = -(x + 1)。所以,如果找不到元素(indexOf 返回 -1),~-1 = -(-1 + 1) = 0,为假;如果找到元素(比如 indexOf 返回索引值 2),~2 = -(2 + 1) = -3 ≠ 0,则为真。这样就能用 if (~arr.indexOf(元素)) 判断是否包含该元素。

  • 代码

    js
    const arr = [1, 2, 3, 4, 5];
    
    // 传统写法
    if (arr.indexOf(3) !== -1) {
      console.log('包含 3');
    }
    
    // 位运算极简写法
    if (~arr.indexOf(3)) {
      console.log('包含 3');
    }

注意

该方法适用于 ES5 及以上环境,并且 indexOf 方法不支持 IE8 及以下。

如果使用 ES6 及以上,也可以使用 arr.includes(元素),但是位运算更快,因为 includes 需要遍历数组,而 indexOf + ~ 只需要 1 次计算。

实现两个数的加法

  • 原理

    利用按位异或(^)和按位与(&)的组合,模拟二进制加法。

    按位异或可以得到“无进位的加法结果”(不同位为 1,相同位为 0,对应加法不进位的情况);按位与可以得到“进位位”(只有两个位都是 1 是才会进位,再左移 1 位,就是进位后的值)。

    重复执行“异或求无进位 + 与运算求进位并左移”,直到进位为 0,此时的无进位和就是最终的加法结果。

  • 代码

    js
    // 不用 + 号,实现两个数的加法
    function add(a, b) {
      while (b !== 0) { // 关键:b 存储的是“进位”,进位为 0 时,加法结束
        // 1. 按位异或得到无进位的加法结果
        const sum = a ^ b; // 比如 5 + 6,5 → 0101,6 → 0110,sum → 0011(无进位和是 3)
        // 2. 按位与并左移得到进位
        const carry = (a & b) << 1; // 5 → 0101,6 → 0110,5 & 6 → 0100,左移 1 位 → 1000(进位是 8)
        // 3. 更新 a 和 b,继续循环
        a = sum; // 3
        b = carry; // 8,不为 0,继续第二轮循环
      }
    
      /* 第二轮循环:
      1. sum = a ^ b(3 → 0011,8 → 1000, sum → 1011,无进位和是 11)
      2. carry = (a & b) << 1(0011 & 1000 → 0000,左移 1 位 → 0000,进位和是 0)
      3. 此时 a = 11,b = 0(循环结束)*/
    
      return a; // 最终 a 就是两个数的和(5 + 6 = 11)
    }
    
    console.log(add(5, 3)); // 8
    console.log(add(7, 8)); // 15
    console.log(add(-2, 4)); // 2
    console.log(add(-10, 5)); // -5
    console.log(add(-4, -6)); // -10
    console.log(add(0, 0)); // 0

提示

该方法完全基于位运算,不使用任何算术运算符,是面试中考察位运算灵活运用的高频难题,理解其逻辑能大幅提升位运算实战能力。

实现两个数的减法

  • 原理

    因为减法的本质就是加法,所以 a - b = a + (-b),而 -b 可以用 ~b + 1(负数的补码是取反加 1),那么减法可以基于上面的 add 函数来实现。

  • 代码

    js
    // 不用 - 号,实现两个数的减法
    function subtract(a, b) {
      // 核心:a - b = a + (-b) = a + (~b + 1)
      return add(a, ~b + 1);
    }
    
    console.log(subtract(8, 3)); // 5
    console.log(subtract(5, 8)); // -3
    console.log(subtract(-2, -4)); // 2

清零二进制指定位

  • 原理

    利用按位与(&)和按位非(~)的组合,实现“只保留需要的二进制位,清零指定位”。其核心逻辑是:先通过按位非将需要清零的位变成 0,其余为变成 1,再与原数字按位与,即可将指定位清零,其余位保持不变。

    比如想清零数字的低 4 位(从右数第 1-4 位),就先创建一个“低 4 位为 0,其余位为 1”的掩码(mask),再 原数字 & mask,即可实现低 4 位清零。

    什么是掩码?

    掩码(Bit Mask)是一串特定的二进制数,它的核心作用是通过与、或、异或等位运算,精确控制目标数字的二进制指定位(如清零、设 1、判断是否为 1),其本质是用二进制的每一位对应一个“标记/权限/状态”,实现高效的位级操作。

    掩码本身是二进制数,每一位(0 或 1)都有明确含义——1 表示“参与位运算”,0 表示“不影响目标数字的对应位”,通过与目标数字做位运算,实现对目标位的精确操作,不干扰其他位。

  • 代码

    js
    // 快速清零二进制指定位(以清零低 4 位为例)
    function clearLow4Bits(n) {
      // 创建掩码:低 4 位为 0,其余位为 1(32 进制)
      const mask = ~0xf; // 0xf 是 15 的十六进制表示,二进制为 0000...1111,~0xf 则为 1111...0000
      return n & mask; // 使用按位与运算清零 n 的低 4 位
    }
    
    console.log(clearLow4Bits(23)); // 23 → 10111,清零低 4 位 → 10000,转为十进制是 16
    console.log(clearLow4Bits(31)); // 31 → 11111,清零低 4 位 → 10000,转为十进制是 16
    console.log(clearLow4Bits(-10)); // -10 补码 → 10110,清零低 4 位 → 10000,转为十进制是 10000 → -16

提示

清零任意指定位,只需修改掩码即可。比如想清零第 8 位(从右数),掩码就是 ~(1 << 7)(1 左移 7 位,对应第 8 位为 1,取反后该为为 0,其余为 1)。

设置二进制指定位为 1

  • 原理

    创建一个“指定位为 1,其余位为 0”的掩码,与原数字按位或,即可将指定位设为 1,其余位不变——因为按位或(|)的特性是有 1 则 1,指定位与 1 结合为 1,其余位与 0 结合保持不变。

    比如想将数字的第 3 位(从右数,对应 22=4)设为 1,就用 1 << 2 生成掩码。

  • 代码

    js
    // 快速设置二进制指定位位 1(以设置第 3 位为例)
    function setBit3(n) {
      const mask = 1 << 2; // 1 → 0001,左移 2 位 → 0100
      return n | mask; // 使用按位或运算将第 3 位设置为 1,其余位保持不变
    }
    
    console.log(setBit3(5)); // 5 → 0101,设置第 3 位后 → 0101,转为十进制是仍然是 5
    console.log(setBit3(6)); // 6 → 0110,设置第 3 位后 → 0110,转为十进制仍然是 6
    console.log(setBit3(0)); // 0 → 0000,设置第 3 位后 → 0100,转为十进制是 4

判断二进制指定位是否为 1

  • 原理

    创建一个“指定位为 1,其余位为 0”的掩码,与原数字按位与。如果结果不为 0,说明指定位是 1;如果结果为 0,说明指定位是 0——因为按位与(&)的特性是全 1 则 1,指定位是 1 与 1 结合为 1,指定位是 0 与 1 结合为 0。

    比如想判断数字的第 4 位(从右数,对应 23=8)是否为 1,就用 1 <<< 3 生成掩码。

  • 代码

    js
    // 快速判断二进制指定位是否为 1(以判断第 4 位为例)
    function isBitSet(n) {
      const mask = 1 << 3; // 1 → 0001,左移 3 位 → 1000
      return (n & mask) !== 0; // 结果非 0,该位为 1,否则为 0
    }
    
    console.log(isBitSet(8)); // 8 → 1000,第 4 位为 1,返回 true
    console.log(isBitSet(12)); // 12 → 1100,第 4 位为 1,返回 true
    console.log(isBitSet(-5)); // -5 → 1011(补码),第 4 位为 1,返回 true

实现权限控制

  • 原理

    利用“位掩码”思想,用二进制的每一位表示一种权限(1 表示拥有该权限,0 表示没有),通过按位或(|)添加权限、按位与(&)判断权限、按位异或(^)切换权限,实现简洁高效的权限管理。该方法比用数组或对象存储权限更节省内存、运算更快。

  • 代码

    js
    // 定义权限位掩码(每一种权限对应二进制的一位,互不重叠)
    const PERMISSION = {
      READ: 1 << 0, // 读权限:0001(第 1 位,对应十进制 1)
      WRITE: 1 << 1, // 写权限:0010(第 2 位,对应十进制 2)
      DELETE: 1 << 2, // 删除权限:0100(第 3 位,对应十进制 4)
      EDIT: 1 << 3 // 编辑权限:1000(第 4 位,对应十进制 8)
    };
    
    // 初始化用户权限
    let userPermission = 0; // 0000,无任何权限
    
    // 添加权限(按位或,只要有一个权限为 1,就拥有该权限)
    userPermission |= PERMISSION.READ; // 添加读权限(0000 | 0001 = 0001,或者 0 | 1 = 1)
    userPermission |= PERMISSION.WRITE; // 添加写权限(0001 | 0010 = 0011,或者 1 | 2 = 3)
    console.log(userPermission); // 输出 3(0011),表示用户拥有读和写权限
    
    // 检查权限(按位与,结果非 0 则拥有该权限)
    const hasRead = (userPermission & PERMISSION.READ) !== 0; // 0011 & 0001 = 0001,非 0
    const hasDelete = (userPermission & PERMISSION.DELETE) !== 0; // 0011 & 0100 = 0000,等于 0
    console.log(hasRead) // true,拥有读权限
    console.log(hasDelete) // false,没有有删除权限
    
    // 移除权限(按位与结合按位非,将对应权限为清零)
    userPermission &= ~PERMISSION.WRITE; // 移除写权限(0011 & 1101 = 0001,或者 3 & ~2 = 1)
    console.log(userPermission); // 输出 1(0001),此时用户只剩读权限
    
    // 切换权限(按位异或,有则移除,无则添加)
    userPermission ^= PERMISSION.READ; // 移除原本有的读权限(0001 ^ 0001 = 0000,或者 1 ^ 1 = 0)
    userPermission ^= PERMISSION.EDIT; // 添加原本没有的编辑权限(0000 ^ 1000 = 1000,或者 0 ^ 8 = 8)
    console.log(userPermission); // 输出 8(1000),此时用户只有编辑权限

提示

该方案广泛应用于后台管理系统、用户权限控制等场景,支持多权限组合、快速判断和修改,代码简洁且性能高效,面试中也常考察该场景的实现思路。

求绝对值

  • 原理

    32 位有符号整数中,正数的绝对值是其本身,负数的绝对值是其补码的“取反加 1”(对应前面讲的补码可逆性)。

    利用右移 31 位获取符号位(正数符号位为 0,负数符号位为 1)。比如 2 → 0010,右移 31 位 → 0000,转为十进制 → 0;-2 → 1110(补码),右移 31 位 → 1111,要转为十进制需要补码逆向操作 → 0001,转为十进制再加上负号 → -1。

    由此可得,正数或 0 右移 31 位的结果是十进制 0,负数右移 31 位的结果是十进制 -1。

    将这个结果作为掩码,再通过公式 (x + 掩码) ^ 掩码,就可以快速计算出绝对值。

  • 代码

    js
    // 利用位运算快速计算绝对值
    function fastAbs(x) {
      const mask = x >> 31; // 获取符号位,正数和 0 为 0,负数为 -1
      return (x + mask) ^ mask; // 如果 x 是正数或 0,返回 x;如果 x 是负数,返回 -x
    }
    
    console.log(fastAbs(0)); // 0 → 0000,右移 31 位 → 0000,mask = 0,(0 + 0) ^ 0 = 0 ^ 0 = 0
    console.log(fastAbs(2)); // 2 → 0010,右移 31 位 → 0000,mask = 0,(2 + 0) ^ 0 = 2 ^ 0 = 2
    console.log(fastAbs(-2)); // -2 → 1110,右移 31 位 → 1111,mask = -1,(-2 + (-1)) ^ (-1) = (-3) ^ (-1) = 2,因为 -3 → 1101,-1 → 1111,1101 ^ 1111 = 0010 = 2
    
    // 事实上,也可以计算小数的绝对值,但是计算前会先自动砍掉小数部分,返回整数部分的绝对值
    console.log(fastAbs(3.28)); // 3
    console.log(fastAbs(-5.7)); // 5
(x + mask) ^ mask 算绝对值的原理

mask 是 通过 x 右移 31 位来获取符号位,转为十进制后,正数或 0 的 mask 为 0,负数的 mask 为 -1。

当 x 为正数或 0 时,(x + mask) ^ mask = (x + 0) ^ 0 = x ^ 0 = x,即正数或 0 的绝对值是其本身。

当 x 为负数时, (x + mask) ^ mask = (x - 1) ^ (-1)。在补码系统中,n ^ (-1) 等价与 ~n(因为 -1 的二进制是全 1),所以 (x + mask) ^ mask = ~(x - 1)

由补码的特性可知 -n = ~n + 1,那么 ~n = -n - 1,所以 ~(x - 1) = -(x - 1) - 1 = -x,正好是 x 的相反数,即负数的绝对值。

限制数字范围

  • 原理

    利用按位与(&)的特性,通过掩码限制数字的范围,避免数值溢出或超出预期区间。该方法通常用来处理颜色值(RGB 值范围 0~255)、像素值等,确保数值在合理区间内。

    比如限制数字在 0~255 之间(8 位无符号整数范围),就用 数字 & 0xff(0xff 是 16 进制,对应 8 位全 1,超过 8 位的部分会被清零)。

  • 代码

    js
    // 限制数字在 0~255 之间
    function clampTo255(n) {
      // 0xff 对应二进制 11111111,按位与后,只保留低 8 位,其余位全部清零
      return n & 0xff;
    }
    
    console.log(clampTo255(300)); // 300 → 100101100,0xff → 11111111,按位与后 → 00101100,即十进制 44
    console.log(clampTo255(200)); // 200 在 0~255 之间,直接返回 200
    console.log(clampTo255(-10)); // -10 → 1111...11110110,按位与后 → 11110110,即十进制 246

补充

限制数字在 0~65535 之间(16 位无符号整数),可以使用 n & 0xffff

限制在 32 位有符号整数范围,可以使用 n >>> 0(无符号右移 0 位)。

但注意,32 位有符号整数范围是 -2147483648~2147483647,如果需要限制在 0~2147483647,可以使用 n & 0x7fffffff(0x7fffffff 是 32 位二进制中,除了最高位(符号位)外,其余 31 位全 1)。

处理 RGB 颜色值

  • 原理

    RGB 颜色值由红(R)、绿(G)、蓝(B)三个分量组成,每个分量的取值范围是 0~255(8 位二进制),正好可以用 32 位整数的低 24 位存储(R、G、B 各占 8 位)。其中 R 分量存储在低 24~17 位,G 分量存储在 16~9 位,B 分量存储在 8~1 位。

    通过位运算的左移(<<)将分量移动到对应位置,通过按位与(&)提取对应分量,从而实现快速拆分、合并 RGB 分量,比字符串拼接、分割更高效。

  • 代码

    js
    // 1. 合并 RGB 分量位一个 32 位整数(便于存储和传输)
    function rgbToInt(r, g, b) {
      // 确保每个分量在 0~255 之间(用前面讲的限制范围方法)
      r = r & 0xff;
      g = g & 0xff;
      b = b & 0xff;
    
      // 左移对应位置,再按位或合并(R 左移 16 位,G 左移 8 位,B 不左移)
      return (r << 16) | (g << 8) | b;
    }
    console.log(rgbToInt(255, 0, 0)) // R → 11111111,G → 00000000,B → 00000000,左移并按位或合并 → 111111110000000000000000,转为十进制 → 16711680
    
    // 2. 从 32 位整数中拆分成 RGB 分量
    function intToRgb(colorInt) {
      const r = (colorInt >> 16) & 0xff; // 右移 16 位,再与 0xff 提取 R 分量
      const g = (colorInt >> 8) & 0xff;  // 右移 8 位,再与 0xff 提取 G 分量
      const b = colorInt & 0xff;         // 直接与 0xff 提取 B 分量
      return { r, g, b };
    }
    console.log(intToRgb(16711680)) // 16711680  → 111111110000000000000000,提取 R → 11111111(255),G → 00000000(0),B → 00000000(0),返回 { r: 255, g: 0, b: 0 }
    
    // 3. 实际应用:修改颜色分量(比如将红色调暗,R 分量减 50)
    let rgbInt = rgbToInt(255, 0, 0); // 获取颜色的整数,16711680
    let redInt = (rgbInt >> 16) & 0xff; // 提取 R 分量,并确保在 0~255 之间,255
    let darkRedInt = (redInt - 50) & 0xff; // 将 R 分量减 50,并确保在 0~255 之间,205
    let newRgbInt = (darkRedInt << 16) | (rgbInt & 0x00ffff); // 重新合并 R 分量,保留 G、B 分量
    /* 上面一行代码中:
    darkRedInt:205 → 00000000 00000000 11001101
    左移 16 位 → 11001101 000000000 0000000 ①
    rgbInt:16711680  → 11111111 00000000 00000000
    0x00ffff → 00000000 11111111 11111111
    rgbInt & 0x00ffff → 00000000 00000000 00000000 ②
    ① | ② → 11001101 00000000 00000000(205, 0, 0)
    */
    console.log(intToRgb(newRgbInt)); // 输出 { r: 205, g: 0, b: 0 }

总结

避坑指南

位运算看似简单,但新手很容易因为“忽略底层细节”踩坑,这里整理了最常见的 5 个错误,结合前面讲的底层知识,可以避开雷区、少走弯路。

  1. 忽略“32 位整数范围”,导致结果异常

    JavaScript 位运算会自动把数字转成 32 位有符号整数,超出范围((231)2311,也就是 -2147483648 到 2147483647)的数字会被自动截断,可能导致结果错误。

  2. 混淆“右移”和“无符号右移”

    新手很容易把两者搞混,记住核心区别:右移(>>)补符号位(负数补 1,正数补 0),结果还是原来的正负;无符号右移(>>>)补 0,负数会变成正数。

  3. 认为“位运算只能用于整数”

    其实位运算可以用于小数,但会自动丢弃小数部分,转成 32 位整数后再运算——比如 5.99 & 1 = 5,但不建议用位运算处理小数,容易造成误解。

  4. 忘记“负数按位运算先转补码”

    所有负数的位运算,都是基于补码进行的,忘记这一点,计算负数的位运算结果会完全错误——比如 ~5 = -6,而不是 4,就是因为补码取反加 1 的原因。

  5. 过度使用位运算

    位运算虽然高效、简洁,但可读性不如传统运算(比如 n & 1 判断奇偶,不如 n % 2 直观)。日常开发中,除非对性能有极高要求,否则优先考虑代码可读性,避免为了“炫技”过度使用位运算。

核心逻辑

最后用 3 句话来总结核心逻辑:

  1. 位运算的本质:直接操作二进制位,比传统运算高效,核心是“32 位有符号整数”和“补码”(负数运算的关键);

  2. 6 种位运算核心:与(&)判断奇偶,或(|)快速取整,异或(^)交换,非(~)取反,左移(>>)乘 2 的 n 次方,右移(<<)除 2 的 n 次方;

  3. 实战关键:记住“特性 + 原理”,不用死记硬背,结合场景套用,同时避开 32 位范围、符号位等坑,就能轻松应对面试和开发。