Skip to content

JavaScript 位运算详解

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

本文将会以 JavaScript 为背景,从底层基础开始,再详细讲解 JavaScript 中的 6 种位运算,最后到实战案例,一步一步吃透位运算的原理。

彻底搞懂二进制

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

十进制和二进制的区别

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

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

十进制二进制解释
00没有就是 0,和十进制一样
11只有 1 个,也和十进制一样
210二进制逢 2 进 1,对应 1 个 2,和 0 个 1,所以是 10
311在 2(10)的基础上加 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 个位置分成两个部分,各司其职:

  1. 第 1 位(最左边)是「符号位」,专门用来区分正数和负数——0 表示正数,1 表示负数;

  2. 剩下的 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 位有符号整数形式(能表示的范围是 2312311,即 -2147483648 到 2147483647),这样计算速度最快,也能满足大部分日常开发的需求。

负数的二进制如何存储

正数的二进制很简单,直接转成二进制,再补够 31 位数值位(不够位数的补 0),最后加上符号位 0,就是计算机的存储形式。

比如 5 的二进制是 101,补齐 31 位数值位得到 0000000 00000000 00000000 00000101,再加上符号位 0 得到 00000000 00000000 00000000 00000101

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

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

补码的计算规则分 3 步:

  1. 先求这个负数的「绝对值」,然后把绝对值转成二进制;

  2. 把这个二进制补成 31 位,不足 31 位的,再前面补 0;

  3. 对这 31 位数值「按位取反」(0 变 1,1 变 0),然后加 1,得到「补码的数值部分」,最后把符号位设为 1(表示负数),就是这个负数的 32 位补码。

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

  1. 求 -5 的绝对值得到 5,转为二进制是 101

  2. 补成 31 位,得到 0000000 00000000 00000000 00000101

  3. 对这 31 位进行按位取反,得到 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 就能得到(11111111 11111111 11111111 11111011 取反得到 00000000 00000000 00000000 00000100,加 1 得到 00000000 00000000 00000000 00000101,也就是 5 的二进制)。

  3. 用公式概括补码的特性就是:-n = ~n + 1 或者 n = ~(-n) + 1

JavaScript 中查看二进制

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

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

// 2. 查看 32 位补码(最精准,适合调试负数、位运算)
function to32BitBinary(n) {
  // 利用「无符号右移 0 位」,强制把数字转成 32 位无符号整数
  return (n >>> 0).toString(2).padStart(32, '0')
}
console.log(to32BitBinary(5)) // 输出 00000000000000000000000000000101
console.log(to32BitBinary(-5)) // 输出 11111111111111111111111111111011

提示

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

6 种位运算

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

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

按位与 &

  • 规则

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

  • 示例 1: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

  • 示例 2:-5 & 3

    注意

    很多人算负数的按位与会懵,其实只要记住「先转补码,再逐位运算,最后转十进制」就能算对。

    1. 把两个数都转成 32 位二进制(这里还是只使用最后 4 位):

      -5 补码 → 1011(前面全是 1),3 → 0011(前面全是 0)

    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 位前面全是 1 & 0 = 0,也就是 0);

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

  • 示例 3:-5 & -3

    注意

    负数按位与的核心,就是「先转补码,再运算」,运算后如果结果的符号位是 0,就是正数;如果符号位是 1,就把结果的补码转成十进制,最后加上负号。

    1. 把两个数都转成 32 位二进制:

      -5 补码 → 1011(前面全是 1),-3 补码 → 1101(前面全是 1)

    2. 对每一位进行按位与运算:

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

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

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

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

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

    4. 因为符号位是 1,说明是负数,所以需要先取反,得到 0110(前面全是 0);

    5. 再加上 1,得到 0111,转成二进制为 7,最后加上负号得到 -7,所以 -5 & -3 = -7

  • 用途

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

    2. 判断某数是否 2 的幂n & (n - 1),结果是 0,n 是 2 的幂;否则不是;

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

按位或 |

  • 规则

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

  • 示例 1:5 | 3

    1. 5 → 0101,3 → 0011;

    2. 从右往左逐位按位或运算:1 | 1 = 10 | 1 = 11 | 0 = 10 | 0 = 0,结果为 0111;

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

  • 示例 2:-5 | 3

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

    2. 逐位按位或运算:1 | 1 = 11 | 1 = 10 | 0 = 01 | 0 = 1,结果为 1011(前面全是 1 | 0,也就是 1);

    3. 结果的符号位是 1,表示是负数。所以结果需要取反加 1, 得 0101,转十进制得 5,加上负号,所以 -5 | 3 = -5

  • 示例 3:-5 | -3

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

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

    3. 结果的符号位是 1,表示是负数。所以结果需要取反加 1, 得 0001,转十进制得 1,加上负号,所以 -5 | -3 = -1

  • 用途

    1. 快速取整(常用):n | 0,比如 5.9 | 0 = 5-2.8 | 0 = -2

    2. 将二进制某一位设为 1:比如想把一个二进制数的最后一位(从左往右数,也就是 20=1 的位置)设为 1,就用 n | 11 除了最后一位是 1,其他位全 0。按位或后,最后一位会变成 1,其他位不变。

按位异或 ^

  • 规则

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

    按位异或有 3 个非常重要的特性:

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

    2. 异或 0 = 自己a ^ 0 = a)——比如 5 ^ 0,转为二进制就是 0101 ^ 0000,结果还是 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

  • 示例 3:-5 ^ -3

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

    2. 逐位按异或运算:1 ^ 1 = 01 ^ 0 = 10 ^ 1 = 11 ^ 1 = 0,结果为 0110;

    3. 前面全是 1 ^ 1 = 0,所以符号位是 0(正数),转十进制得 6,所以 -5 ^ -3 = 6

  • 用途

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

    2. 反转二进制某一位:比如想反转最后一位(0 变 1、1 变 0),就用 n ^ 1,因为 1 的二进制是 0001,只有最后一位是 1,其他位全 0。按位异或后,最后一位会变成 1 ^ 1 = 00 ^ 1 = 1,其他位不变。

按位非 ~

  • 规则

    对一个数字的 32 位二进制(或补码),每一位都取反:0 变 1,1 变 0(包括符号位)。换句话说就是反向操作

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

  • 示例 1:~5

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

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

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

      5 的 32 位二进制为 0101(最后 4 位,前面全是 0),按位非后变成 1010(前面全是 1);

      转换后的符号位是 1(负数),转为十进制后需要加上负号,即 -6,所以 ~5 = -6

  • 示例 2:~-5

    • 方法 1:用公式

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

    • 方法 2:用二进制

      -5 的补码为 1011(最后 4 位,前面全是 1),按位非后变成 0100(前面全是 0);

      转为十进制为 4,所以 ~-5 = 4

  • 用途

    1. 快速判断素组是否包含某个元素(适用于 ES5 及以上环境,并且不支持 IE8 及以下);

    2. 使用 ~x = -(x + 1) 公式快速计算按位非结果。

左移 <<

  • 规则

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

    比如,3 → 0011,左移 1 位得到 0110 → 6;左移 2 位得到 1100 → 12。

    可以看出,左移 n 位,就相当于乘以 2 的 n 次方,换句话说就是翻 n 番。转换为公式就是:

    x<<n=x×2n

  • 示例 1:3 << 1

    • 方法 1:用公式

      3<<1=3×21=6

    • 方法 2:用二进制

      3 → 0011,左移 1 位,左边移出的 0 丢弃,右边空出来的位补 0,得到 0110 → 6。所以 3 << 1 = 6

  • 示例 2:-3 << 1

    • 方法 1:用公式

      3<<1=3×21=6

    • 方法 2:用二进制

      -3 补码 → 1101,左移 1 位,左边移出的 1 丢弃,右边空出来的位补 0,得到 1010 → 6;

      因为符号位是 1,所以需要加上负号,结果为 -6。所以 -3 << 1 = 6

  • 用途

    1. 快速乘以 2 的 n 次方:比如 4 << 2 = 4 x 4 = 16

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

右移 >>

  • 规则

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

    比如,8 → 1000,右移 1 位得到 0100 → 4;右移 2 位得到 0010 → 2。

    可以看出,右移 n 位,就相当于除以 2 的 n 次方

    注意

    如果不是整除会如何处理?

    比如 7 >> 1,7 → 0111,右移 1 位,右边移出去的 1 丢弃,左边空出来的位补 0,会得到 0011 → 3。注意,不是 3.5,而是 3,因为位运算只保留整数部分。所以,7 >> 1 = 3

    再比如 -7 >> 1,7 补码 → 1001,右移 1 位,右边移出去的 1 丢弃,左边空出来的位补 1,会得到 1100。因为符号位是 1(负数),转换成十进制前需要取反加 1,所以得到 0100 → 4,最后加上负号得 -4。所以 -7 >> 1 = -4

    由上面两个例子可以看出,在右移时,如果不能被 2 的 n 次方整除,结果会自动向下取整。

    根据上面的分析,右移转换成公式就是:

    x>>n=x÷2n (向下取整的数学符号是 ‌⌊ ⌋‌,通常记作 ‌⌊x⌋‌)

    补充

    还有一种右移,叫无符号右移 >>>。它与右移 >> 的区别是:左边空出来的位,不管是正数还是负数,前面空出来的位一律补 0。所以负数无符号右移后,会变成正数。

    前面用来查看二进制的方法就是用了无符号右移。其中 n >>> 0 的作用就是,把数字强制转成 32 位无符号整数,这样就能看到计算机实际存储的 32 位二进制,不会像 toString(2) 那样简化显示。

  • 示例 1:8 >> 1

    8 → 1000,右移 1 位后,右边移出去的 0 丢弃,左边空出的位补 0,得到 0100 → 4。所以 8 >> 1 = 4。和公式 8>>1=8÷21=4 计算的结果一致。

  • 示例 2:-8 >> 1

    8 补码 → 1000,右移 1 位后,右边移出去的 0 丢弃,左边空出的位补 1,得到 1100。因为符号位是 1,所以需要取反加 1,得到 0100 → 4,最后加上负号得 -4。所以 -8 >> 1 = -4。和公式 8>>1=8÷21=4 计算的结果一致。

  • 示例 3:8 >>> 1

    8 >> 1 结果一致,都是 4。因为 8 是正数,无符号右移和有符号右移左边都是补 0。

  • 示例 4:-8 >>> 1

    -8 补码 → 1000,向右移动 1 位,左边补 0,结果会变成 01111111...11111100(完整的 32 位)。因为符号位变成了 0,转成十进制就是正数 2147483644。

  • 用途

    1. 快速除以 2 的 n 次方:比如 16 >> 2 = 16 / 4 = 4

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

实战案例

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

判断奇偶

  • 原理

    要判断一个数 n 是奇数还是偶数,使用 n & 1 即可。

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

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

    由此可见 n & 1 的结果只能是 0 或 1。若结果是 0,说明 n 的 32 位二进制的最后一位是 0,则 n 是偶数;若结果是 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)

快速取整

  • 原理

    要对一个数 n 快速取整,使用 n | 0 即可。

    首先你要知道,在 JavaScript 中使用任何位运算时,会先强制将数字转成 32 位有符号整数,自动去掉小数部分。

    因为按位或的规则是有 1 为 1,所以使用 n | 0 时,会保持 n 不变。

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

  • 代码

    js
    function floor(n) {
      // 任何数与 0 进行按位或运算时,都会得到它自身
      return n | 0
    }
    
    console.log(floor(5.99)) // 5
    console.log(floor(-2.8)) // -2

交换两个数字

  • 原理

    利用按位异或的特性,可以不使用临时变量 temp,就能实现两个数字的交换。比如要交换 a 和 b 两个数字,具体步骤如下:

    1. a = a ^ b——此时 a 存储了 a 和 b 的差异(因为异或只有不同才是 1,所以结果中的位,如果是 0,说明该位没有差异;如果是 1,说明该位不一样);

    2. b = a ^ b——经过上一步,此时的 a 已经变成了 a ^ b。那么,b = a ^ b = (a ^ b) ^ b,根据异或的特性 (a ^ b) ^ b = a ^ (b ^ b) = a ^ 0 = a,所以 b 最终变成了 a

    3. a = a ^ b——同样的,此时的 b 已经变成了 a,那么,a = a ^ b = a ^ (a ^ b) = (a ^ a) ^ b = 0 ^ b = b,所以 a 最终变成了 b

    分析

    为什么第 1 步中 a 已经改变了(a = a ^ b),而第 2 步中得到的结果 a 是原来的值,而不是改变后的值?

    你可以这么理解:

    1. a = 旧a ^ 旧b

    2. 新b = a ^ 旧b = (旧a ^ 旧b) ^ 旧b

      根据异或的特性,旧b ^ 旧b = 0,所以 新b = 旧a ^ 0 = 旧a

    3. 新a = a ^ 新b = (旧a ^ 旧b) ^ 旧a

      根据异或的特性,旧a ^ 旧a = 0,所以 新a = 0 ^ 旧b = 旧b

    使用公式概括就是:一个数字 ^ 两个数字的差异 = 另一个数字

  • 代码

    js
    // 使用按位异或的方法交换两个数字,不用额外占用内存(无临时变量)
    // 是面试中「代码优化」的常见考点
    function swap(a, b) {
      a = a ^ b // 存储 a 和 b 的差异
      b = a ^ b // 原始 b 异或差异值,得到原始 a
      a = a ^ b // 原始 a 异或差异值,得到原始 b
      return [a, b]
    }
    
    let [x, y] = swap(3, 5)
    
    console.log(x, y) // 5 3

判断某数是否 2 的幂

  • 原理

    判断一个数 n 是否是 2 的幂,使用 n & (n - 1) 即可。

    首先你要知道:2 的幂的二进制有一个核心特征——只有一位是 1,其余全是 0(比如 2 → 10,4 → 100,8 → 1000)。

    如果一个数 n 是 2 的幂,那么 n - 1 的二进制会把这个唯一的 1 变成 0,并且它后面的所有 0 都变成 1(比如 8 → 1000,那么 8 - 1 = 7 → 0111)。

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

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

    反过来分析,如果 n 不是 2 的幂,那么 n 的二进制中必然会有多个 1,n & (n - 1) 的结果就不会是 0(比如 6 → 0110,5 → 0101,0110 & 0101 = 0100 ≠ 0)。

  • 代码

    js
    function isPowerOfTwo(n) {
      // 注意:n 必须大于 0,因为 0 和负数都不是 2 的幂
      return n > 0 && (n & (n - 1)) === 0
    }
    
    console.log(isPowerOfTwo(4)) // true (0100 & 0011 = 0000 = 0)
    console.log(isPowerOfTwo(6)) // false (0110 & 0101 = 0100 ≠ 0)
    console.log(isPowerOfTwo(8)) // true (1000 & 0111 = 0000 = 0)
    // 1 是 2 的 0 次方,并且 1 的二进制就是 1,符合「只有一个 1 的特征」,所以返回 true
    console.log(isPowerOfTwo(1)) // true (0001 & 0000 = 0000 = 0)

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

  • 规则

    首先,你需要了解 JavaScript 中数组的 indexOf 方法——该方法用于查找数组元素。如果找到,返回元素的索引(≥ 0);如果没找到,返回 -1。

    然后使用按位非计算 indexOf 的返回值(前面说过,按位非的公式是 ~x = -(x + 1))。如果数组包含该元素,indexOf 返回的索引 ≥ 0,按位非后结果 ≤ -1(真);如果没找到,indexOf 返回 -1,按位非后结果为 0(假)。

    这样,就能使用 if (~arr.indexOf(item)) 判断是否包含该元素。

    注意

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

    如果使用 ES6 及以上,也可以使用 arr.includes(item) 方法直接返回布尔值。

    但是,位运算更快,因为 includes 方法需要遍历数组,而位运算直接计算,不需要遍历。

  • 代码

    js
    const arr = [1, 2, 3, 4]
    
    if (~arr.indexOf(2)) {
      console.log('数组中包含元素 2')
    }

实现两个数的加法

  • 原理

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

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

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

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

  • 代码

    js
    function add(a, b) {
      // 关键:b 存储的是「进位」,进位为 0 时加法结束
      // 以 a = 5,b = 6 为例,开始第一轮循环
      while (b !== 0) {
        // 1. 按位异或得到无进位的加法结果
        const sum = a ^ b // 0101 ^ 0110 = 0011(无进位和是 3)
        // 2. 按位与并左移得到进位
        const carry = (a & b) << 1 // (0101 & 0110) << 1 = 1000(进位和是 8)
        // 3. 更新 a 和 b
        a = sum // 3
        b = carry // 8,不为 0,继续第二轮循环
      }
    
      /**
       * 第二轮循环
       * 1. sum = 0011 ^ 1000 = 1011,无进位和是 11
       * 2. carry = (0011 & 1000) << 1 = 0000,进位和是 0
       * 3. a = 11,b = 0,结束循环
       */
    
      return a // 最终 a 就是两个数的和 11(即 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(由按位或的公式 ~x = -(x + 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 位(即最后 4 位),就先创建一个「低 4 位为 0,其余位为 1」的掩码(mask),再 原数字 & mask,即可实现低 4 位清零。

    什么是掩码?

    掩码又叫位掩码(Bit Mask),是一串特定的二进制数。

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

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

    比如,想清零一个二进制数从右往左数的第 8 位,掩码可以设置为 ~(1 << 7)。1 右移 7 位可得 10000000,取反后得 01111111,此时对应的第 8 位为 0,其余位为 1。

    此时,不论二进制数的第 8 位是 0 还是 1,与掩码的第 8 位 0 做与运算时,都会得到 0,从而达到对应位清零其他位不变的效果。

  • 代码

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

设置二进制指定位为 1

  • 原理

    创建一个「指定位位 1,其余位为 0」的掩码,与原数字按位或,即可将指定位设置为 1,其余位保持不变。

    因为按位或(|)的规则是有 1 则 1,指定位与 1 结合为 1,其余位与 0 结合保持不变。

  • 代码

    js
    // 快速设置二进制从右数第 3 位为 1
    function setBit3(n) {
      const mask = 1 << 2 // 1 → 0001,左移 2 位 → 0100
      return n | mask // 使用按位与运算将 n 的第 3 位设置为 1,其他位保持不变
    }
    
    console.log(setBit3(0)) // 0 → 0000,0000 | 0100 = 0100 = 4
    console.log(setBit3(2)) // 2 → 0010,0010 | 0100 = 0110 = 6
    console.log(setBit3(5)) // 5 → 0101,0101 | 0100 = 0101 = 5

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

  • 原理

    创建一个「指定位为 1,其余位为 0」的掩码,与原数字按位与。如果结果为 0,说明指定位为 0;如果结果不为 0,说明指定位为 1。

    按位与(&)的规则是全 1 则 1,指定位是 1 与 1 结合为 1,指定位是 0 与 1 结合为 0。

  • 代码

    js
    // 快速判断二进制从右数第 4 位是否为 1
    function isBit4Set(n) {
      const mask = 1 << 3 // 1 → 0001,左移 3 位 → 1000
      return (n & mask) !== 0 // 结果非 0,该位为 1,否则为 0
    }
    
    console.log(isBit4Set(6)) // 6 → 0110,0110 & 1000 = 0000 = 0,false
    console.log(isBit4Set(12)) // 12 → 1100,1100 & 1000 = 1000 = 8,true
    console.log(isBit4Set(-5)) // -5 补码 → 1011,1011 & 1000 = 1000 = 8,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
    userPermission |= PERMISSION.WRITE // 添加写权限:0001 | 0010 = 0011
    console.log(userPermission) // 输出 3(即 0011,表示用户拥有读写权限)
    
    // 检查权限(按位与,结果非 0,则拥有该权限)
    const hasRead = (userPermission & PERMISSION.READ) !== 0
    const hasDelete = (userPermission & PERMISSION.DELETE) !== 0
    console.log(hasRead) // 0011 & 0001 = 0001(非 0,true,拥有读权限)
    console.log(hasDelete) // 0011 & 0100 = 0000(0,false,没有删除权限)
    
    // 移除权限(按位与 + 按位取反,将对应位清零)
    userPermission &= ~PERMISSION.WRITE // 移除读权限:0011 & ~0010 = 0011 & 1101 = 0001
    console.log(userPermission) // 输出 1(即 0001,此时用户只剩读权限)
    
    // 切换权限(按位异或,有则移除,无则添加)
    userPermission ^= PERMISSION.READ // 切换读权限:0001 ^ 0001 = 0000(移除读权限)
    userPermission ^= PERMISSION.EDIT // 切换读权限:0000 ^ 1000 = 1000(添加编辑权限)
    console.log(userPermission) // 输出 8(即 1000,此时用户只有编辑权限)

求绝对值

  • 原理

    首先,利用右移 31 位获取符号位(即确定需要求绝对值的数是正数或 0,还是负数)。比如 2 → 0010,右移 31 位得 0000,结果是十进制 0,表示是正数或 0;-2 补码 → 1111,右移 31 位得 1111,结果是十进制 -1,表示是负数。

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

    (x + mask) ^ mask 算绝对值的原理

    当 x 为正数或 0 时,mask = 0。 此时 (x + 0) ^ 0 = x ^ 0 = x,即 x 本身。

    当 x 为负数时,mask = -1。此时 (x + (-1)) ^ (-1) = (x - 1) ^ (-1)

    n ^ (-1)~n 是等价的,因为 -1 的补码是全 1。所以,(x - 1) ^ (-1) = ~(x - 1)

    由补码的特性可知 -n = ~n + 1,那么 ~n = -n - 1

    所以 ~(x - 1) = -(x - 1) - 1 = -x,正好是 x 的相反数,即负数的绝对值。

  • 代码

    js
    function fastAbs(n) {
      const mask = n >> 31 // 获取符号位。结果为 0 表示正数或 0,结果为 -1 表示负数
      return (n + mask) ^ mask // 如果 n 是正数或 0,返回 n;如果 n 是负数,返回 -n
    }
    
    console.log(fastAbs(0)) // mask = 0 >> 31 = 0, return 0 ^ 0 = 0
    console.log(fastAbs(2)) // mask = 2 >> 31 = 0, return 2 ^ 0 = 2
    console.log(fastAbs(-2)) // mask = -2 >> 31 = -1, return (-3) ^ (-1) = 2
    
    // 事实上,也可以传入小数,但是会先自动砍掉小数部分,返回整数部分的绝对值
    console.log(fastAbs(3.28)) // 3
    console.log(fastAbs(-5.7)) // 5

限制数字范围

  • 原理

    利用按位与(&)的规则,通过掩码限制数字的范围,避免数值溢出或超出预期区间。

    该方法通常用来处理颜色值(RGB 值范围 0~255)、像素值等,确保数值在合理区间内。

  • 代码

    js
    // 限制数字在 0~255 之间
    function clampTo255(n) {
      // 0xff 对应 32 位二进制 0000...11111111,且对应十进制 255
      // 使用 0xff 作为掩码的作用是,只保留低 8 位,其余位全部清零
      return n & 0xff
    }
    
    console.log(clampTo255(300)) // 300 → 0000...100101100,和 0xff 按位与得 0000...00101100 → 44
    console.log(clampTo255(200)) // 200 → 0000...11001000,和 0xff 按位与得 0000...11001000 → 200
    console.log(clampTo255(-10)) // -10 → 1111...11110110,和 0xff 按位与得 0000...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 之间
      // 0xff 对应二进制低 8 位全 1,其余位 0
      r &= 0xff
      g &= 0xff
      b &= 0xff
    
      // 左移对应位置,再按位或合并
      return (r << 16) | (g << 8) | b
    }
    console.log(rgbToInt(255, 0, 0)) // 完整颜色值:00000000 11111111 00000000 00000000 → 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)) // { r: 255, g: 0, b: 0 }
    
    // 3. 实际应用:将红色调暗 50(即将 R 分量减 50)
    const { r, g, b } = intToRgb(16711680) // 提取各分量的值
    const dimmedR = r - 50 // R 分量减 50,G、B 分量不变
    const colorInt = rgbToInt(dimmedR, g, b) // 合并为新的颜色值
    console.log(colorInt) // 13434880
    const colorObj = intToRgb(colorInt) // 转换为新的颜色对象
    console.log(colorObj) // { r: 205, g: 0, b: 0 }

总结

避坑指南

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

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

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

  2. 混淆「右移」和「无符号右移」

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

  3. 认为「位运算只能用于整数」

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

  4. 忘记「负数按位运算先转补码」

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

  5. 过度使用位运算

    位运算虽然高效、简洁,但可读性不如传统运算——比如 n & 1 判断奇偶,不如 n % 2 直观。日常开发中,除非对性能有极高要求,否则优先考虑代码可读性。

核心逻辑

最后,用 3 菊花来总结核心逻辑:

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

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

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