JavaScript 位运算解读

207 阅读14分钟

1. 引言

位运算是一种计算机程序设计中对二进制数进行操作的基本方法。与直接对数值进行运算的算术运算不同,位运算是针对数字的二进制表示形式中的每一个比特位进行逻辑操作。理解位运算对于深入了解计算机底层工作原理以及进行某些特定的优化和数据处理至关重要。在 JavaScript 中,虽然所有数字都以 64 位浮点数的形式存储,但当执行位运算时,JavaScript 引擎会将这些数值转换为 32 位有符号整数进行处理,运算结束后再将结果转换回 JavaScript 的数字类型。本文旨在对 JavaScript 中的位运算进行全面的解读,涵盖其基本原理、各类运算符的功能、实际应用场景以及性能考量,为 JavaScript 开发者提供深入的理解和应用指导。

2. 二进制表示与位逻辑基础

要理解位运算,首先需要掌握二进制数的概念。二进制是一种以 2 为基数的计数系统,只使用数字 0 和 1 来表示数值,这与我们日常生活中使用的十进制(以 10 为基数,使用 0-9)不同。计算机内部所有的数据最终都以二进制形式存储和处理。例如,十进制数 5 在二进制中表示为 0101,而十进制数 3 则表示为 0011。在 JavaScript 中,可以使用 toString(2) 方法将一个十进制数转换为其二进制字符串表示。

位运算是基于二进制数的每一位进行逻辑操作的。最基础的位逻辑运算包括与(AND)、或(OR)、非(NOT)和异或(XOR)。它们的逻辑规则可以用真值表来概括:

ABA & BA | BA ^ B
00000
01011
10011
11110

此外,JavaScript 使用补码(two's complement)系统来表示有符号整数。补码表示法对于理解位非(NOT)运算符在负数上的行为至关重要。在补码系统中,负数的二进制表示是通过将对应的正数的二进制表示按位取反(得到反码),然后加 1 得到的。这解释了为什么在 JavaScript 中,对一个数 x 进行位非运算 ~x 的结果等同于 -(x + 1)

3. JavaScript 位运算符详解

JavaScript 提供了七种位运算符,可以对整数的二进制表示进行操作。

3.1 位与(AND)运算符 (&)

位与运算符 & 对两个操作数的每一位执行逻辑与操作。只有当两个操作数对应位都为 1 时,结果的该位才为 1,否则为 0。例如,5 & 3 的运算过程是:0101 (5 的二进制) 与 0011 (3 的二进制) 进行位与操作,结果是 0001,即十进制的 1。

位与运算符的常见用途包括:

  • 掩码(Masking): 通过与一个特定的“掩码”进行位与操作,可以用来提取数字中的特定位。掩码是一个二进制数,其中需要提取的位为 1,其余位为 0。例如,要提取数字的最后两位,可以与掩码 0011 进行位与操作。这种方法允许开发者有选择地获取整数中编码的特定信息。
  • 检查特定位是否被设置: 将数字与一个只在需要检查的位上为 1 的掩码进行位与操作。如果结果非零,则表示该位已被设置。这为检查由位表示的单个标志的状态提供了一种高效的方式。
  • 判断奇偶性: 通过将数字与 1 进行位与操作。如果结果为 1,则该数字是奇数;如果结果为 0,则该数字是偶数。这是因为二进制数最后一位为 1 时是奇数,为 0 时是偶数。

3.2 位或(OR)运算符 (|)

位或运算符 | 对两个操作数的每一位执行逻辑或操作。只要两个操作数对应位中至少有一个为 1,结果的该位就为 1,否则为 0。例如,5 | 3 的运算过程是:0101 (5 的二进制) 与 0011 (3 的二进制) 进行位或操作,结果是 0111,即十进制的 7。

位或运算符的常见用途包括:

  • 设置特定位: 通过与一个在需要设置的位上为 1 的掩码进行位或操作,可以将数字的特定位设置为 1 2。如果该位已经是 1,则保持不变;如果是 0,则变为 1。
  • 合并标志(Combining Flags): 当使用位掩码表示多个布尔标志时,可以使用位或运算符将多个标志合并到一个整数中。每个标志通常用 2 的幂表示,这样每个标志对应一个唯一的比特位。

3.3 位异或(XOR)运算符 (^)

位异或运算符 ^ 对两个操作数的每一位执行逻辑异或操作 1。只有当两个操作数对应位不同时,结果的该位才为 1,否则为 0。例如,5 ^ 3 的运算过程是:0101 (5 的二进制) 与 0011 (3 的二进制) 进行位异或操作,结果是 0110,即十进制的 6。

位异或运算符的常见用途包括:

  • 翻转特定位(Toggling Bits): 通过与一个在需要翻转的位上为 1 的掩码进行位异或操作,可以翻转数字的特定位 2。如果该位是 0,则变为 1;如果是 1,则变为 0。

  • 比较位: 判断两个数的对应位是否不同。

  • 不使用临时变量交换两个数值: 可以使用位异或运算来实现。例如:

    let a = 5;
    let b = 3;
    a ^= b;
    b ^= a;
    a ^= b;
    // 现在 a 是 3,b 是 5
    

3.4 位非(NOT)运算符 (~)

位非运算符 ~ 是一元运算符,它对操作数的每一位执行逻辑非操作。也就是说,0 变为 1,1 变为 0。例如,~5 的运算过程是:00000000000000000000000000000101 (5 的 32 位二进制表示) 进行位非操作,结果是 11111111111111111111111111111010,即十进制的 -6。需要注意的是,~x 等价于 -(x + 1)

位非运算符的常见用途包括:

  • 创建特定模式的掩码: 可以用来快速创建除了特定位之外所有位都为 1 的掩码。
  • indexOf() 方法结合进行布尔检查: 例如,~"abc".indexOf("b") 会返回一个真值,因为 indexOf() 返回 1,~1 是 -2,在布尔上下文中为真。如果子字符串未找到,indexOf() 返回 -1,~-1 是 0,在布尔上下文中为假。然而,现代 JavaScript 中通常更倾向于使用 includes() 方法来提高代码的可读性。

3.5 左移运算符 (<<)

左移运算符 << 将第一个操作数的二进制表示向左移动第二个操作数指定的位数。左移后空出的右侧位用零填充,而移出左侧的位则被丢弃。例如,5 << 2 的运算过程是:0101 (5 的二进制) 向左移动 2 位,结果是 010100,即十进制的 20。

左移运算符的常见用途包括:

  • 乘以 2 的幂: 将一个数左移 n 位等价于将该数乘以 2^n。在某些情况下,这比直接使用乘法运算符效率更高。
  • 创建位掩码: 将 1 左移 n 位可以创建一个只有第 (n+1) 位为 1 的掩码。

3.6 有符号右移运算符 (>>)

有符号右移运算符 >> 将第一个操作数的二进制表示向右移动第二个操作数指定的位数。右移后空出的左侧位用符号位(即最高位)的值填充,因此可以保持原始数值的符号。移出右侧的位则被丢弃。例如,10 >> 2 的运算过程是:1010 (10 的二进制) 向右移动 2 位,结果是 0010,即十进制的 2。-10 >> 2 的运算过程是:-10 的 32 位二进制表示向右移动 2 位,左侧用符号位 1 填充,结果是 -3。

有符号右移运算符的常见用途包括:

  • 除以 2 的幂(保留符号): 将一个数右移 n 位等价于将该数除以 2^n 并向下取整,同时保留原始数值的符号。

3.7 零填充右移运算符 (>>>)

零填充右移运算符 >>> 将第一个操作数的二进制表示向右移动第二个操作数指定的位数。右移后空出的左侧位总是用零填充,无论原始数值的符号如何。移出右侧的位则被丢弃。因此,该运算符的结果总是非负数。例如,10 >>> 2 的结果是 2,与 >> 相同。但是,对于负数,结果会有所不同。-10 >>> 2 的结果是一个很大的正数,因为左侧填充的是零。需要注意的是,零填充右移运算符不能用于 BigInt 类型。

零填充右移运算符的常见用途包括:

  • 无符号除以 2 的幂: 对于正数,其行为与 >> 相同。对于负数,它执行的是无符号的右移操作,结果会是一个很大的正数。
  • 将数值强制转换为 32 位无符号整数: 例如,x >>> 0 可以将 x 转换为 32 位无符号整数。

以下表格总结了 JavaScript 中的位运算符:

运算符名称描述示例常见用途
&位与两个操作数对应位都为 1 时,结果该位为 1,否则为 0。5 & 3掩码,检查位是否设置,判断奇偶性
|位或两个操作数对应位至少有一个为 1 时,结果该位为 1,否则为 0。5 | 3设置特定位,合并标志(Combining Flags)
^位异或两个操作数对应位不同时,结果该位为 1,否则为 0。5 ^ 3翻转特定位,比较位,不使用临时变量交换数值
~位非反转操作数的每一位。~5创建特定掩码,与 indexOf() 结合进行布尔检查
<<左移将第一个操作数的二进制表示向左移动指定位数,右侧补 0。5 << 2乘以 2 的幂,创建位掩码
>>有符号右移将第一个操作数的二进制表示向右移动指定位数,左侧补符号位。10 >> 2除以 2 的幂(保留符号)
>>>零填充右移将第一个操作数的二进制表示向右移动指定位数,左侧补 0。10 >>> 2无符号除以 2 的幂,强制转换为 32 位无符号整数

4. JavaScript 位运算的实际应用

位运算在 JavaScript 中有着广泛的应用,尤其在需要高效地处理多个布尔值、进行性能优化以及处理底层数据时。

4.1 位掩码技术与示例

位掩码是一种使用整数的不同比特位来表示一组布尔标志或选项的技术。与使用多个布尔变量相比,使用单个整数可以更有效地存储和管理这些标志。每个标志通常被赋予一个 2 的幂的值,这样每个标志对应整数中的一个唯一比特位。

例如,考虑一个表示用户偏好的配置管理场景。我们可以使用一个整数来存储用户是否在线、是否全屏、是否有音频以及是否拥有高级账户等信息。每个偏好对应一个比特位:

const IS_ONLINE = 1 << 0;   // 0001 (1)
const IS_FULLSCREEN = 1 << 1; // 0010 (2)
const HAS_AUDIO = 1 << 2;    // 0100 (4)
const HAS_PREMIUM = 1 << 3;  // 1000 (8)

let userPreferences = 0;

// 设置用户在线
userPreferences |= IS_ONLINE;

// 检查用户是否拥有高级账户
const hasPremium = (userPreferences & HAS_PREMIUM)!== 0;

// 清除用户在线状态
userPreferences &= ~IS_ONLINE;

// 切换全屏状态
userPreferences ^= IS_FULLSCREEN;

通过位与运算符 & 和一个掩码,可以检查特定的标志是否被设置。使用位或运算符 | 可以设置一个标志。使用位与和位非运算符 & ~ 可以清除一个标志。使用位异或运算符 ^ 可以切换一个标志的状态。位掩码技术在存储权限管理、UI 组件状态等方面非常有用。

4.2 位运算在性能优化中的应用

左移和右移运算符可以用于快速地进行乘以或除以 2 的幂的运算。例如,x << 3 等价于 x * 8,而 x >> 1 等价于 Math.floor(x / 2)。在某些低级语言中,位运算通常比算术运算更快。然而,在现代 JavaScript 引擎中,这种性能差异可能并不显著,因为引擎本身可能会进行类似的优化。尽管如此,在一些对性能要求极高的场景下,例如游戏开发或实时数据处理,位运算仍然可能是一个考虑因素。

4.3 处理二进制数据

位运算在处理二进制数据时扮演着关键角色。例如,在图形处理中,颜色通常以 32 位整数的形式存储,其中不同的位段表示颜色的不同分量(如 Alpha、Red、Green、Blue)。通过使用位移和位与运算,可以高效地提取和修改这些颜色分量。

const color = 0xFFAABBCC; // ARGB 格式

const alpha = (color >>> 24) & 0xFF; // 提取 Alpha 分量
const red   = (color >>> 16) & 0xFF; // 提取 Red 分量
const green = (color >>> 8)  & 0xFF; // 提取 Green 分量
const blue  = color         & 0xFF; // 提取 Blue 分量

类似地,在文件解析和网络通信等领域,数据通常以二进制流的形式存在,位运算是解析和构建这些底层数据结构的基础。

4.4 高级应用

位运算也应用于更高级的计算机科学领域。例如,异或运算 ^ 在简单的加密算法(如异或密码)中有所应用。位运算也常用于错误检测和纠正算法,例如奇偶校验和循环冗余校验(CRC)。此外,在某些特定的算法优化中,位运算也能够提供高效的解决方案。

5. 常见的位操作技巧

除了直接使用位运算符外,还有一些常见的位操作技巧可以简化代码并提高效率:

  • 检查第 n 位是否设置: (number >> n) & 1(number & (1 << n))!== 0
  • 设置第 n 位: number | (1 << n)
  • 清除第 n 位: number & ~(1 << n)
  • 切换第 n 位: number ^ (1 << n)

这些技巧可以帮助开发者更方便地操作整数中的特定比特位。

6. JavaScript 中的性能考量与最佳实践

虽然位运算在处理器层面通常非常快速,但在 JavaScript 中,由于需要将 64 位浮点数转换为 32 位整数进行运算,这可能会引入一定的开销。在许多常见的 Web 开发场景中,这种性能差异可能并不明显。

因此,开发者应该优先考虑代码的可读性和可维护性,而不是过早地使用位运算进行优化,特别是当性能提升不显著时 。在决定使用位运算进行优化之前,最好先通过性能分析工具来确定代码中的实际瓶颈。

7. JavaScript 中 BigInt 的位运算

JavaScript 的 BigInt 类型用于处理任意精度的整数,它也支持位运算。与标准数字类型不同,BigInt 的位运算直接在其任意精度的整数表示上进行,没有 32 位限制。需要注意的是,零填充右移运算符 >>> 没有为 BigInt 类型定义,因为 BigInt 在概念上具有无限数量的前导位,因此用零填充没有明确的意义。BigInt 支持位与 &、位或 |、位异或 ^、位非 ~、左移 << 和有符号右移 >> 等运算符。BigInt 的位运算为处理超出标准数字范围的超大整数提供了强大的功能。

8. 结论

JavaScript 中的位运算提供了一种直接操作数字二进制表示形式的强大机制。理解这些运算符的功能和应用场景,能够帮助开发者在需要进行低级数据处理、优化特定操作或管理多个布尔标志时编写出更高效和精巧的代码。虽然在现代 JavaScript 开发中,位运算可能不如其他高级特性那样常用,但掌握它们仍然是完善开发者工具箱的重要组成部分。开发者应根据实际需求和性能考量,合理地运用位运算,并在追求性能的同时,不牺牲代码的可读性和可维护性。