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 位有符号整数形式(能表示的范围是 −231 到 231−1,即 -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 = 1、0 | 1 = 1、1 | 0 = 1、0 | 0 = 0,结果为 0111;

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

  • 示例 2:-5 | 3

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

    2. 逐位按位或运算:1 | 1 = 1、1 | 1 = 1、0 | 0 = 0、1 | 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 = 1、1 | 0 = 1、0 | 1 = 1、1 | 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 | 1。1 除了最后一位是 1,其他位全 0。按位或后,最后一位会变成 1,其他位不变。

按位异或 ^ ​

  • 规则

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

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

    1. 自己异或自己 = 0(a ^ 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 = 0、0 ^ 1 = 1、1 ^ 0 = 1、0 ^ 0 = 0,结果为 0110;

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

  • 示例 2:-5 ^ 3

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

    2. 逐位按异或运算:1 ^ 1 = 0、1 ^ 1 = 0、0 ^ 0 = 0、1 ^ 0 = 1,结果为 1000;

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

  • 示例 3:-5 ^ -3

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

    2. 逐位按异或运算:1 ^ 1 = 0、1 ^ 0 = 1、0 ^ 1 = 1、1 ^ 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 = 0 或 0 ^ 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 = 5、7 >> 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 位有符号整数,超出范围(−231 到 231−1,也就是 -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 位范围、符号位等坑,就能轻松应对面试和开发。