JavaScript 位运算详解
位运算的本质就是“直接和计算机对话”——计算机只认识 0 和 1(二进制),就像我们只认识汉字、英文一样,位运算就是跳过“十进制转二进制”的中间步骤,直接操作这些 0 和 1,即快又省内存,一点都不复杂。
本文将会从底层基础到 6 种位运算,再到实战案例,一步一步带你吃透位运算的原理。
彻底搞懂二进制
位运算的所有操作,都基于「二进制」——计算机的“母语”。本章节先把二进制是什么、怎么来的、计算机如何存储、正负数如何区分这些问题全部讲解清楚,后面学习位运算就会水到渠成,不用死记硬背,理解了底层,规则自然就会了。
十进制和二进制的区别
我们平时计数、花钱、算账,使用的都是「十进制」,原因很简单——我们有 10 根手指,数到 10 就没法再数了,只能进 1 位,这就是“逢 10 进 1”。
而计算机没有“手指”,它的核心是晶体管,晶体管只有“通电”和“断电”两种状态,正好对应 0(断电)和 1(通电)。所以计算机只能用二进制计数,数到 2 就进 1 位,这就是“逢 2 进 1”。
| 十进制 | 二进制 | 解释 |
|---|---|---|
| 0 | 0 | 没有就是 0,和十进制一样 |
| 1 | 1 | 只有 1 个,记位 1,也和十进制一样 |
| 2 | 10 | 二进制逢 2 进 1,对应“1 个 2,和 0 个 1”,所以是 10 |
| 3 | 11 | 在 2(10)的基础上再加 1 个 1,就是 “1 个 2,和 1 个 1”,所以是 11 |
| 4 | 100 | 再逢 2 进 1,对应“1 个 4,0 个 2,和 0 个 1”,所以是 100 |
| 5 | 101 | 4(100)加 1,就是“1 个 4,0 个 2,和 1 个 1”,所以是 101 |
| 6 | 110 | 4(100)加 2,就是“1 个 4,1 个 2,和 0 个 1”,所以是 110 |
| 7 | 111 | 4(100)加 2(10)加 1(1),三个位都是 1,所以是 111 |
| 8 | 1000 | 再逢 2 进 1,对应“1 个 8,0 个 4,0 个 2,和 0 个 1”,所以是 1000 |
记住两个核心:
二进制的每一位,都叫「比特位(bit)」,从右往左数,第 1 位是
(也就是 1),第 2 位是 (也就是 2),第 3 位是 (也就是 4),第 4 位是 (也就是 8)……以此类推(右边是低位,左边是高位,位数越往左,数值越大)。 二进制转十进制,直接“拆位相加”即可。比如二进制 101,拆解就是:
再比如二进制 110,拆解就是:
反过来,十进制转二进制也不用复杂计算,记住“除以 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 位整数能表示的范围是:
负数的二进制如何存储
正数的二进制很简单,直接转成二进制,再补够 31 位数值位,加上符号位 0,就是计算机的存储形式。但负数的二进制,计算机不会直接存“负号 + 绝对值”(比如 -5,不会存 -101),而是用「补码」存储——这也是为什么很多人算“按位非”或“负数位运算”会懵的核心原因,搞懂补码,负数运算就再也不怕了。
先搞清楚一个小问题:为什么计算机不用“负号 + 绝对值”存负数?因为计算机只有加法器,没有减法器,用补码可以把“减法”变成“加法”,计算更简单(比如
补码的计算规则分 3 步:
- 先求这个负数的「绝对值」,然后把绝对值转成二进制;
- 把这个二进制补成 31 位(符号位占 1 位,数值位只有 31 位),不足 31 位的,在前面补 0;
- 对这 31 位数值位「按位取反」(0 变 1,1 变 0),然后加 1,得到“补码的数值部分”,最后把符号位设为 1(表示负数),就是这个负数的 32 位补码。
下面用一个例子来说明补码的详细步骤,比如十进制 -5:
- 求 -5 的绝对值得到 5,5 的二进制是 101;
- 补成 31 位数值位得到
0000000 00000000 00000000 00000101; - 按位取反得到
1111111 11111111 11111111 11111010; - 取反后加 1 得到
1111111 11111111 11111111 11111011; - 最后加上符号位 1(最左边)得到
11111111 11111111 11111111 11111011,这就是 -5 的 32 位二进制存储形式,也就是补码。
注意
- 计算机存储负数,只存补码,不存“负号 + 绝对值”。所有位运算(包括按位非、按位与等)都是基于补码进行的——后面学“按位非”时,我们就会明白,为什么
~5 = -6,本质就是补码取反加 1 的过程。 - 补码的“取反加 1”是可逆的。比如知道 -5 的补码,想求它的绝对值,只要对补码的数值部分再取反加 1,就能得到 5 的二进制(
11111111 11111111 11111111 11111011取反得到00000000 00000000 00000000 00000100,加 1 就是00000000 00000000 00000000 00000101,也就是 5 的二进制)。 - 用公式概括补码的特性就是:
-n = ~n + 1或者n = ~(-n) + 1。
JavaScript 中查看二进制
JavaScript 中不用手动换算二进制、补码,用 2 个简单方法,就能快速查看数字的二进制形式,方便调试、验证位运算结果。
// 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把 5 和 3 都转成 32 位二进制(为了方便看,这里只使用最后 4 位,前面全是 0,不影响结果):
5 → 0101
3 → 0011
对应每一位进行“按位与”运算(从右往左,逐位比较):
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;
把运算后的每一位组合起来,得到二进制结果 0001(完整 32 位前面全是 0);
把二进制结果转成十进制:0001 → 1,所以
5 & 3 = 1。
补充
很多人算负数的按位与会懵,其实只要记住“先转补码,再逐位运算,最后转十进制”,一步都不跳,就能算对。我们以计算
-5 & 3为例,拆解每一步:把两个数都转成 32 位二进制(-5 转补码,3 转正数二进制):
-5 的补码:11111111 11111111 11111111 11111011(最后 4 位 1011)
3 的 32 位二进制:00000000 00000000 00000000 00000011(最后 4 位 0011)
对应每一位进行按位与运算(逐位比较,符号位也参与运算):
-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;
把运算后的每一位组合起来,得到二进制结果 0011(完整 32 位前面全是 0);
把二进制结果转成十进制: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。用途
判断奇偶(最常用):
n & 1,如果结果是 1,就是奇数;结果是 0,就是偶数;将二进制某一位设为 0:比如想把一个数的第 4 位(从左往右数,也就是
的位置)设为 0,就用 n & ~1,~1是全 1 除了第 4 位是 0,按位与后,第 4 位就会变成 0。
按位或 |
规则
两个数字的 32 位二进制,对应每一位进行比较。只要有一个位是 1,结果位就是 1;只有两个位都是 0,结果位才是 0。
就像“满足一个条件就能做某事”——比如去景区买票,学生证或身份证只要有一个就能买票,如果两个都没有就买不了。
换句话说就是有真为真。
示例 1:计算
5 | 35 的 32 位二进制(最后 4 位)为 0101;3 的 32 位二进制(最后 4 位)为 0011;
逐位按位或运算:
1 | 1 = 1、0 | 1 = 1、1 | 0 = 1、0 | 0 = 0,二进制结果为 0111;转十进制:0111 → 7,所以
5 | 3 = 7。
示例 2:计算
-5 | 3-5 的 32 位补码(最后 4 位)为 1011;3 的 32 位二进制(最后 4 位)为 0011;
逐位按位或运算:
1 | 1 = 1、1 | 1 = 1、0 | 0 = 0、1 | 0 = 1,二进制结果为 1011;结果符号位是 1(负数),转补码(取反加 1)得到 0101,转十进制是 5,加上负号,所以
-5 | 3 = -5。
用途
快速取整(比
Math.floor快,常用):任意数字 | 0,就能去掉小数部分,比如5.99 | 0 = 5、-2.8 | 0 = -2;权限组合(经典用途):用不同的二进制表示不同权限,按位或可以合并多个权限(比如读权限 1 + 写权限 2,
1 | 2 = 3,3 就表示同时有读和写的权限);将二进制某一位设为 1:比如想把一个数的第 4 位(
的位置)设为 1,就用 n | 1,1的二进制是 0001,按位或后,第 4 位就会变成 1,其他位不变。
按位异或 ^
规则
两个数字的 32 位二进制,对应每一位进行比较。两个位不一样(一个 0、一个 1),结果位就是 1;两个位一样(都是 0 或都是 1),结果位就是 0。
就像“两个人不一样才可以组队”——甲乙必须是一男一女才能组队,甲乙都是男或都是女则不能组队。
换句话说就是不同为真。
按位异或有 3 个非常实用的特性:
自己异或自己 = 0(
a ^ a = 0)——比如5 ^ 5,转为二进制就是0101 ^ 0101,每一位都一样,结果都是 0,所以5 ^ 5 = 0;异或 0 = 自己(
a ^ 0 = a)——比如5 ^ 0,转为二进制就是0101 ^ 0000,每一位都和 0 对比,不一样的就是自己,结果还是 0101,所以5 ^ 0 = 5;可交换、可结合(
a ^ b ^ c = a ^ c ^ b = (a ^ b) ^ c)——这个特性是“不用临时变量交换两个数”的核心。
示例 1:计算
5 ^ 35 → 0101,3 → 0011;
逐位异或运算:
1 ^ 1 = 0、0 ^ 1 = 1、1 ^ 0 = 1、0 ^ 0 = 0,二进制结果为 0110;转十进制:0110 → 6,所以
5 ^ 3 = 6。
示例 2:计算
-5 ^ 3-5 补码 → 1011,3 → 0011;
逐位异或运算:
1 ^ 1 = 0、1 ^ 1 = 0、0 ^ 0 = 0、1 ^ 0 = 1,二进制结果位 1000;结果符号位是 1(负数),转十进制(取反加 1)为 1000 → 8,加上负号,所以
-5 ^ 3 = -8。
用途
不用临时变量交换两个数(面试常考);
反转二进制某一位:比如想反转第 4 位(0 变 1、1 变 0),就用
n ^ 1,异或后,第 4 位会反转,其他位不变。
按位非 ~
规则
对一个数字的 32 位二进制(或补码),每一位都取反:0 变 1,1 变 0(包括符号位)。
就像“开关的反向操作”——开关开着,按一下就关了;开关关着,按一下就开了。所有开关都要反向。
换句话说就是反向操作。
提示
这个公式能快速算出按位非的结果,不用再转二进制、取反、转十进制,节省时间。 示例 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,和公式结果一致。
用途
快速判断数组是否包含某个元素:
arr.indexOf(元素)返回索引,找不到返回 -1,~-1 = 0;找到返回 ≥ 0 的索引,~索引 ≠ 0,所以if (~arr.indexOf(元素))就能判断是否包含该元素;快速取反(比如把 0 变成 -1、1 变成 -2,本质就是用公式
~x = -(x + 1));
左移 <<
规则
把一个数字的 32 位二进制(或补码),整体向左移动 n 位,左边移出的位置直接丢弃,右边空出来的位补 0。
就像“计数翻倍”——比如有 3 个苹果(二进制 0011),翻倍一次(左移 1 位)变成 6 个(0110),再翻倍一次(左移 1 位)变成 12 个(1100)。和“乘以 2 的 n 次方”完全对应。
换句话说就是翻倍。
提示
这个公式能快速算出左移的结果,不用再转二进制、移动、转十进制,节省时间。 示例 1(计算
3 << 1):方法 1:用公式
方法 2:用二进制
3 的 32 位二进制(最后 4 位)为 0011,向左移动 1 位,左边移出的 0 丢弃,右边补 0,结果为 0110,转十进制就是 6,结果与公式一致。
示例 2(计算
-3 << 1):方法 1:用公式
方法 2:用二进制
-3 的补码(最后 4 位)为 1101,向左移动 1 位,左边移出的 1 丢弃,右边补 0,结果为 1010,符号位 1(负数),转十进制为 0110 → 6,加上负号为 -6,结果与公式一致。
用途
快速乘以 2 的 n 次方(比乘法运算快):比如
4 << 2 = 4 x 4 = 16;快速生成 2 的 n 次方:比如
1 << 3 = 8;权限设置:比如想设置第 n 位权限(从右数),就用
1 << (n - 1)。
右移 >>
规则
把一个数字的 32 位二进制(或补码),整体向右移动 n 位,右边移出的位置直接丢弃,左边空出来的位补符号位(正数补 0,负数补 1)。
就像“计数减半”——比如有 8 个苹果(二进制 1000),减半一次(右移 1 位)变成 4 个(0100),再减半一次(右移 1 位)变成 2 个(0010)。和“除以 2 的 n 次方”完全对应。
换句话说就是减半。
提示
这个公式能快速算出左移的结果,不用再转二进制、移动、转十进制,节省时间。 补充
无符号右移
>>>和 右移>>的区别是,左边空出来的位不管符号位,一律补 0,所以负数无符号右移后,会变成正数。前面用来查看补码的方法就是用了无符号右移。示例 1:计算
8 >> 18 的 32 位二进制(最后 4 位)位 1000,向右移动 1 位,右边移出的 0 丢弃,左边补 0(正数),结果为 0100,转十进制就是 4,所以
8 >> 1 = 4,和规律一致。 示例 2:计算
-8 >> 18 的补码(最后 4 位)位 1000,向右移动 1 位,右边移出的 0 丢弃,左边补 1(负数),结果为 1100,转十进制就是 0100 → 4,加上负号就是 -4,所以
-8 >> 1 = -4,和规律一致。 示例 3:计算
8 >>> 1和-8 >>> 1正数无符号右移:
8 >>> 1和8 >> 1结果一致,都是 4,因为左边都是补 0;负数无符号右移:-8 的补码是
11111111 11111111 11111111 11111000,向右移动 1 位,左边补 0,结果是01111111 11111111 11111111 11111100,转为十进制为 2147483644(正数,符号位变成 0)。这就是无符号右移和右移的核心区别。
用途
快速除以 2 的 n 次方(比除法运算快,且自动向下取整):比如
, ; 获取数字的一半(向下取整):比如
10 >> 1 = 5,7 >> 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 是奇数。代码
jsfunction 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,就能实现两个数字的交换。
拆解过程:
a = a ^ b——此时 a 存储了 a 和 b 的差异(因为异或只有不同才是 1,相当于记录了 a 和 b 哪些位不一样);b = a ^ b——a ^ b就是原来的 a(因为 a 现在是a ^ b,a ^ b ^ b = a ^ (b ^ b) = a ^ 0 = a);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)。代码
jsfunction 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 次方(
判断数组是否包含某个元素
原理
首先需要了解数组的 indexOf 方法——找不到元素时返回 -1,找到时返回 ≥0 的索引;
然后按位非计算 indexOf 的返回值——按位非的公式是
~x = -(x + 1)。所以,如果找不到元素(indexOf 返回 -1),~-1 = -(-1 + 1) = 0,为假;如果找到元素(比如 indexOf 返回索引值 2),~2 = -(2 + 1) = -3 ≠ 0,则为真。这样就能用if (~arr.indexOf(元素))判断是否包含该元素。代码
jsconst 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 位(从右数,对应
)设为 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 位(从右数,对应
)是否为 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 个错误,结合前面讲的底层知识,可以避开雷区、少走弯路。
忽略“32 位整数范围”,导致结果异常
JavaScript 位运算会自动把数字转成 32 位有符号整数,超出范围(
到 ,也就是 -2147483648 到 2147483647)的数字会被自动截断,可能导致结果错误。 混淆“右移”和“无符号右移”
新手很容易把两者搞混,记住核心区别:右移(
>>)补符号位(负数补 1,正数补 0),结果还是原来的正负;无符号右移(>>>)补 0,负数会变成正数。认为“位运算只能用于整数”
其实位运算可以用于小数,但会自动丢弃小数部分,转成 32 位整数后再运算——比如
5.99 & 1 = 5,但不建议用位运算处理小数,容易造成误解。忘记“负数按位运算先转补码”
所有负数的位运算,都是基于补码进行的,忘记这一点,计算负数的位运算结果会完全错误——比如
~5 = -6,而不是 4,就是因为补码取反加 1 的原因。过度使用位运算
位运算虽然高效、简洁,但可读性不如传统运算(比如
n & 1判断奇偶,不如n % 2直观)。日常开发中,除非对性能有极高要求,否则优先考虑代码可读性,避免为了“炫技”过度使用位运算。
核心逻辑
最后用 3 句话来总结核心逻辑:
位运算的本质:直接操作二进制位,比传统运算高效,核心是“32 位有符号整数”和“补码”(负数运算的关键);
6 种位运算核心:与(
&)判断奇偶,或(|)快速取整,异或(^)交换,非(~)取反,左移(>>)乘 2 的 n 次方,右移(<<)除 2 的 n 次方;实战关键:记住“特性 + 原理”,不用死记硬背,结合场景套用,同时避开 32 位范围、符号位等坑,就能轻松应对面试和开发。