JavaScript 位运算详解
位运算的本质就是「直接和计算机对话」——计算机只认识 0 和 1(二进制),就像我们只认识汉字、英文一样,位运算就是跳过「十进制转二进制」的中间步骤,直接操作这些 0 和 1,既快又省内存,一点都不复杂。
本文将会以 JavaScript 为背景,从底层基础开始,再详细讲解 JavaScript 中的 6 种位运算,最后到实战案例,一步一步吃透位运算的原理。
彻底搞懂二进制
位运算的所有操作,都基于二进制——计算机的「母语」。本章先把二进制是什么、怎么来的、计算机如何存储、正负数如何区分这些问题全部讲解清楚,后面学习位运算就会水到渠成,不用死记硬背。理解了底层,规则自然就会了。
十进制和二进制的区别
我们平时计数、花钱、算账,使用的都是十进制,原因很简单——我们有十根手指,数到 10 就没法再数了,只能进一位,这就是「逢 10 进 1」。
而计算机没有「手指」,它的核心是晶体管,晶体管只有「通电」和「断电」两种状态,正好对应 0(断电)和 1(通电)。所以计算机只能用二进制计数,数到 2 就进一位,也就是「逢 2 进 1」。
| 十进制 | 二进制 | 解释 |
|---|---|---|
| 0 | 0 | 没有就是 0,和十进制一样 |
| 1 | 1 | 只有 1 个,也和十进制一样 |
| 2 | 10 | 二进制逢 2 进 1,对应 1 个 2,和 0 个 1,所以是 10 |
| 3 | 11 | 在 2(10)的基础上加 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 个位置分成两个部分,各司其职:
第 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 位有符号整数形式(能表示的范围是
负数的二进制如何存储
正数的二进制很简单,直接转成二进制,再补够 31 位数值位(不够位数的补 0),最后加上符号位 0,就是计算机的存储形式。
比如 5 的二进制是 101,补齐 31 位数值位得到 0000000 00000000 00000000 00000101,再加上符号位 0 得到 00000000 00000000 00000000 00000101。
但负数的二进制,计算机不会直接存「负号 + 绝对值」(比如 -5,不会存 -101),而是用采用了「补码」的形式存储——这也是为什么很多人算「按位非」或「负数位运算」会懵的核心原因。搞懂补码,负数运算就再也不怕了。
先搞清楚一个小问题:为什么计算机不用「负号 + 绝对值」存负数?因为计算机只有加法器,没有减法器,用补码可以把减法变成加法(比如 5 - 3,相当于 5 + (-3)),运算更简单,这是补码的核心作用。
补码的计算规则分 3 步:
先求这个负数的「绝对值」,然后把绝对值转成二进制;
把这个二进制补成 31 位,不足 31 位的,再前面补 0;
对这 31 位数值「按位取反」(0 变 1,1 变 0),然后加 1,得到「补码的数值部分」,最后把符号位设为 1(表示负数),就是这个负数的 32 位补码。
下面用一个例子来说明补码的详细步骤,比如十进制 -5:
求 -5 的绝对值得到 5,转为二进制是
101;补成 31 位,得到
0000000 00000000 00000000 00000101;对这 31 位进行按位取反,得到
1111111 11111111 11111111 11111010;将取反后的结果加 1,得到
1111111 11111111 11111111 11111011;最后在最左边加上符号位 1,得到
11111111 11111111 11111111 11111011,这就是 -5 的 32 位二进制存储形式,也就是补码。
注意
计算机存储负数,只存补码,不存「负号 + 绝对值」。所有位运算(包括按位非、按位与等)都是基于补码进行的——后面学「按位非」时,我们就会明白,为什么
~5 = -6,本质就是补码取反加 1 的过程。补码的取反加 1 是可逆的。比如,知道 -5 的补码,想求它的绝对值,只要对补码的数值部分再取反加 1 就能得到(
11111111 11111111 11111111 11111011取反得到00000000 00000000 00000000 00000100,加 1 得到00000000 00000000 00000000 00000101,也就是 5 的二进制)。用公式概括补码的特性就是:
-n = ~n + 1或者n = ~(-n) + 1。
JavaScript 中查看二进制
JavaScript 中不用手动换算二进制、补码,用两个简单的方法,就能快速查看数字的二进制形式,方便调试、验证位运算结果。
// 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把 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。
示例 2:
-5 & 3注意
很多人算负数的按位与会懵,其实只要记住「先转补码,再逐位运算,最后转十进制」就能算对。
把两个数都转成 32 位二进制(这里还是只使用最后 4 位):
-5 补码 → 1011(前面全是 1),3 → 0011(前面全是 0)
对每一位进行按位与运算:
-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 位前面全是
1 & 0 = 0,也就是 0);把二进制结果转成十进制:0011 → 3,所以
-5 & 3 = 3。
示例 3:
-5 & -3注意
负数按位与的核心,就是「先转补码,再运算」,运算后如果结果的符号位是 0,就是正数;如果符号位是 1,就把结果的补码转成十进制,最后加上负号。
把两个数都转成 32 位二进制:
-5 补码 → 1011(前面全是 1),-3 补码 → 1101(前面全是 1)
对每一位进行按位与运算:
-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;把运算后的每一位组合起来,得到二进制结果 1001(完整 32 位前面全是 1);
因为符号位是 1,说明是负数,所以需要先取反,得到 0110(前面全是 0);
再加上 1,得到 0111,转成二进制为 7,最后加上负号得到 -7,所以
-5 & -3 = -7。
用途
判断奇偶(最常用):
n & 1,结果是 1,n 为奇数;结果是 0,n 为偶数;判断某数是否 2 的幂:
n & (n - 1),结果是 0,n 是 2 的幂;否则不是;将二进制某一位设为 0:比如想把一个二进制数的最后一位(从左往右数,也就是
的位置)设为 0,就用 n & ~1。~1除了最后一位是 0,其他位全 1。按位与后,最后一位会变成 0,其他位不变。
按位或 |
规则
两个数字的 32 位二进制,对应每一位进行比较。只要有一个位是 1,结果位就是 1;只有两个位都是 0,结果为才是 0。换句话说,就是有 1 为 1,或有真为真。
示例 1:
5 | 35 → 0101,3 → 0011;
从右往左逐位按位或运算:
1 | 1 = 1、0 | 1 = 1、1 | 0 = 1、0 | 0 = 0,结果为 0111;转十进制:0111 → 7,所以
5 | 3 = 7。
示例 2:
-5 | 3-5 补码 → 1011,3 → 0011;
逐位按位或运算:
1 | 1 = 1、1 | 1 = 1、0 | 0 = 0、1 | 0 = 1,结果为 1011(前面全是1 | 0,也就是 1);结果的符号位是 1,表示是负数。所以结果需要取反加 1, 得 0101,转十进制得 5,加上负号,所以
-5 | 3 = -5。
示例 3:
-5 | -3-5 补码 → 1011,-3 补码 → 1101;
逐位按位或运算:
1 | 1 = 1、1 | 0 = 1、0 | 1 = 1、1 | 1 = 1,结果为 1111;结果的符号位是 1,表示是负数。所以结果需要取反加 1, 得 0001,转十进制得 1,加上负号,所以
-5 | -3 = -1。
用途
快速取整(常用):
n | 0,比如5.9 | 0 = 5、-2.8 | 0 = -2;将二进制某一位设为 1:比如想把一个二进制数的最后一位(从左往右数,也就是
的位置)设为 1,就用 n | 1。1除了最后一位是 1,其他位全 0。按位或后,最后一位会变成 1,其他位不变。
按位异或 ^
规则
两个数字的 32 位二进制,对应每一位进行比较。两个位不一样(一个 0、一个 1),结果位是 1;两个位一样(都是 0,或都是 1),结果位是 0。换句话说,就是不同为 1,或不同为真。
按位异或有 3 个非常重要的特性:
自己异或自己 = 0(
a ^ a = 0)——比如5 ^ 5,转为二进制就是0101 ^ 0101,每一位都一样,结果都是 0,所以5 ^ 5 = 0;异或 0 = 自己(
a ^ 0 = a)——比如5 ^ 0,转为二进制就是0101 ^ 0000,结果还是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。
示例 3:
-5 ^ -3-5 补码 → 1011,-3 补码 → 1101;
逐位按异或运算:
1 ^ 1 = 0、1 ^ 0 = 1、0 ^ 1 = 1、1 ^ 1 = 0,结果为 0110;前面全是
1 ^ 1 = 0,所以符号位是 0(正数),转十进制得 6,所以-5 ^ -3 = 6。
用途
不用临时变量交换两个数(面试常考);
反转二进制某一位:比如想反转最后一位(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。
用途
快速判断素组是否包含某个元素(适用于 ES5 及以上环境,并且不支持 IE8 及以下);
使用
~x = -(x + 1)公式快速计算按位非结果。
左移 <<
规则
把一个数字的 32 位二进制(或补码),整体向左移动 n 位,左边移出去的位置直接丢弃,右边空出来的位补 0。
比如,3 → 0011,左移 1 位得到 0110 → 6;左移 2 位得到 1100 → 12。
可以看出,左移 n 位,就相当于乘以 2 的 n 次方,换句话说就是翻 n 番。转换为公式就是:
示例 1:
3 << 1方法 1:用公式
方法 2:用二进制
3 → 0011,左移 1 位,左边移出的 0 丢弃,右边空出来的位补 0,得到 0110 → 6。所以
3 << 1 = 6。
示例 2:
-3 << 1方法 1:用公式
方法 2:用二进制
-3 补码 → 1101,左移 1 位,左边移出的 1 丢弃,右边空出来的位补 0,得到 1010 → 6;
因为符号位是 1,所以需要加上负号,结果为 -6。所以
-3 << 1 = 6。
用途
快速乘以 2 的 n 次方:比如
4 << 2 = 4 x 4 = 16;快速生成 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⌋)补充
还有一种右移,叫无符号右移
>>>。它与右移>>的区别是:左边空出来的位,不管是正数还是负数,前面空出来的位一律补 0。所以负数无符号右移后,会变成正数。前面用来查看二进制的方法就是用了无符号右移。其中
n >>> 0的作用就是,把数字强制转成 32 位无符号整数,这样就能看到计算机实际存储的 32 位二进制,不会像toString(2)那样简化显示。示例 1:
8 >> 18 → 1000,右移 1 位后,右边移出去的 0 丢弃,左边空出的位补 0,得到 0100 → 4。所以
8 >> 1 = 4。和公式计算的结果一致。 示例 2:
-8 >> 18 补码 → 1000,右移 1 位后,右边移出去的 0 丢弃,左边空出的位补 1,得到 1100。因为符号位是 1,所以需要取反加 1,得到 0100 → 4,最后加上负号得 -4。所以
-8 >> 1 = -4。和公式计算的结果一致。 示例 3:
8 >>> 1和
8 >> 1结果一致,都是 4。因为 8 是正数,无符号右移和有符号右移左边都是补 0。示例 4:
-8 >>> 1-8 补码 → 1000,向右移动 1 位,左边补 0,结果会变成
01111111...11111100(完整的 32 位)。因为符号位变成了 0,转成十进制就是正数 2147483644。用途
快速除以 2 的 n 次方:比如
16 >> 2 = 16 / 4 = 4;获取数字的一半(向下取整):比如
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 是奇数。该方法使用位运算直接操作了二进制,比普通的取余运算(%)更快,且对负数判断更精准,代码更简洁。
代码
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)
快速取整
原理
要对一个数 n 快速取整,使用
n | 0即可。首先你要知道,在 JavaScript 中使用任何位运算时,会先强制将数字转成 32 位有符号整数,自动去掉小数部分。
因为按位或的规则是有 1 为 1,所以使用
n | 0时,会保持 n 不变。该方法仅适用于 32 位整数范围内的数字(-2147483648 ~ 2147483647),超出范围会出现异常。日常开发中,大部分场景都在这个范围内。
代码
jsfunction floor(n) { // 任何数与 0 进行按位或运算时,都会得到它自身 return n | 0 } console.log(floor(5.99)) // 5 console.log(floor(-2.8)) // -2
交换两个数字
原理
利用按位异或的特性,可以不使用临时变量 temp,就能实现两个数字的交换。比如要交换 a 和 b 两个数字,具体步骤如下:
a = a ^ b——此时 a 存储了 a 和 b 的差异(因为异或只有不同才是 1,所以结果中的位,如果是 0,说明该位没有差异;如果是 1,说明该位不一样);b = a ^ b——经过上一步,此时的 a 已经变成了a ^ b。那么,b = a ^ b = (a ^ b) ^ b,根据异或的特性(a ^ b) ^ b = a ^ (b ^ b) = a ^ 0 = a,所以b最终变成了a;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 是原来的值,而不是改变后的值?你可以这么理解:
a = 旧a ^ 旧b;新b = a ^ 旧b = (旧a ^ 旧b) ^ 旧b根据异或的特性,
旧b ^ 旧b = 0,所以新b = 旧a ^ 0 = 旧a;新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)。代码
jsfunction 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 方法需要遍历数组,而位运算直接计算,不需要遍历。
代码
jsconst arr = [1, 2, 3, 4] if (~arr.indexOf(2)) { console.log('数组中包含元素 2') }
实现两个数的加法
原理
利用按位异或(
^)和按位与(&)的组合,模拟二进制加法。按位异或可以得到「无进位的加法结果」(不同位为 1,相同位为 0,对应加法不进位的情况);按位与可以得到「进位位」(只有两个位都是 1 时才会进位,再左移 1 位,就是进位后的值)。
重复执行「异或求无进位 + 与运算求进位并左移」,直到进位为 0,此时的无进位和就是最终的加法结果。
该方法完全基于位运算,不使用任何算术运算符,是面试中考察位运算灵活运用的高频难题。理解其逻辑能大幅提升位运算实战能力。
代码
jsfunction 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 函数来实现了。代码
jsfunction 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 的相反数,即负数的绝对值。代码
jsfunction 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 个最常见的错误,结合前面讲的底层知识,可以避开雷区、少走弯路。
忽略「32 位整数范围」,导致结果异常
JavaScript 位运算会自动把数字转成 32 位有符号整数,超出范围(
到 ,也就是 -2147483648 到 2147483647)的数字会被自动截断,可能导致错误结果。 混淆「右移」和「无符号右移」
新手很容易把两者搞混,记住核心区别:右移(
>>)补符号位(正数补 0,负数补 1),结果还是原来的正负;无符号右移(>>>)不分正负,一律补 0,导致负数会变成正数。认为「位运算只能用于整数」
其实位运算可以用于小数,但会自动丢弃小数部分,转成 32 位整数后再运算——比如
5.99 & 1 = 5。但不建议用位运算处理小数,容易造成误解。忘记「负数按位运算先转补码」
所有负数的位运算,都是基于补码进行的。忘记这一点,计算负数的位运算结果会完全错误——比如
~5 = -6,就是因为补码取反加 1 的原因。过度使用位运算
位运算虽然高效、简洁,但可读性不如传统运算——比如
n & 1判断奇偶,不如n % 2直观。日常开发中,除非对性能有极高要求,否则优先考虑代码可读性。
核心逻辑
最后,用 3 菊花来总结核心逻辑:
位运算的本质:直接操作二进制位,比传统运算高效,核心是「32 位有符号整数」和「补码」(负数运算的关键);
6 种位运算核心:与(
&)判断奇偶,或(|)快速取整,异或(^)交换,非(~)取反,左移(<<)乘 2 的 n 次方,右移(>>)除 2 的 n 次方;实战关键:记住「特性 + 原理」,不用死记硬背,结合场景套用,同时避开 32 位范围、符号位等坑,就能轻松应对面试和开发。