二十、位操作

位操作是 JavaScript 开发人员通常不需要了解的高级主题。像 C 这样的低级编程语言就利用了这些运算符。但是,如果您想实现高性能的服务器端代码,您应该学习一点位操作。

理解位操作需要一些数字逻辑知识。任何离散数学或电路的入门课程都有助于理解这些概念。

按位运算符

以下是 JavaScript 中的按位运算符:

  • &:和

  • |:或者

  • ~:不是

  • ^:异或

  • <<:/>

  • :右移

  • :零填充右移

注意

回想一下第 3 章,所有的数字都用 32 位表示(意味着有 32 个 1 和 0)。将十进制数(基数为 10)转换为二进制数(基数为 2)时,记住这一点很重要。

当两位都为 1 时,AND操作符为真。&(与号)用于AND操作符。

a        b        a AND b
0        0        0
0        1        0
1        0        0
1        1        1

在按位运算中,数字用二进制表示。比如二进制的 9 是1001,二进制的 5 是101

对于每个位,必须执行AND操作:

9:          0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1
5:          0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1
9 & 5:      0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 = 1

1   console.log(9 & 5); //  prints 1

这是另一个例子:

40 in base 10 = 100010 in base 2
41 in base 10 = 100011 in base 2

40:       0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0
41:       0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 1

40 & 41:  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0 =  40

运筹学

当任一位为 1 时,OR操作符为。|(管子)用于OR操作器。

a        b        a OR b
0        0        0
0        1        1
1        0        1
1        1        1

我们以9 | 540 | 41为例。

9:     0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1
5:     0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1
9 | 5: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 1 = 13

这是另一个例子:

40:         0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0
41:         0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 1
40 & 41:    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 1 = 41

异或

XOR意为“异或”只有当其中一个位为 1 时,它才计算为真。^(插入符号)用于XOR操作符。

a        b        a XOR b
0        0        0
0        1        1
1        0        1
1        1        0

9:          0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1
5:          0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1
9 ^ 5:      0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 = 12

40:         0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0
41:         0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 1
40 ^ 41:    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 =  1

NOT运算符反转所有位。~(波浪号)用于NOT操作符。请不要混淆NOT运算符和负运算符。一旦这些位反转,32 位中的数字也随之反转。

a        NOT a
0        1
1        0

我们以 9 和 5 为例:

9:    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1
~9:   1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 0 = -10

5:    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1
~5:   1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 0 = -6

左移

在左移位中,所有的位都向左移位,任何向左移位的多余位都被丢弃。<<(双左尖括号)是左移操作符。

9:      0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1
9 << 1: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 0 = 18
9 << 2: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 0 0 = 36

左移位通常是每次移位将元素乘以 2。这是因为二进制是以 2 为基数的系统,意味着左移等于所有数字乘以 2。但是,移位可能会导致位溢出并减少值。

1073741833:       0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1
1073741833 << 2:  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 0 0 = 36

右移

在右移位中,所有的位都向右移位,任何向右移位的多余位都被丢弃。>>(双右尖括号)是右移操作符。

9:          0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1
9 >> 1:     0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 = 4

-9:         1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1
-9 >> 2:    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 = -3

在这个例子中,移位将 9 除以 2(整数除法)。这也是因为二进制是以 2 为基数的系统。

零填充右移

在零填充右移位中,所有的位都向右移位,任何向右移位的多余位都被丢弃。然而,符号位(最左边的位)在移位前变成 0,这导致非负数。>>>(三个右括号)是零填充右移的操作符。

-9:        1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1
-9 >>> 1:  0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 = 2147483643

在这个例子中,移位将 9 除以 2(整数除法)。这也是因为二进制是以 2 为基数的系统。

为了更好地理解为什么这些操作有效,建议在学校或网上学习数字逻辑入门课程。最后,一切都由 1 和 0 组成,因为计算机中的晶体管只能有两种状态:开和关。

数字运算

本节将介绍如何使用按位运算符执行加、减、乘、除和取模运算。

添加

二进制数相加和十进制数相加没什么区别。孩子们在二年级学习的规则是一样的:将两个数字相加,如果超过 10,就将 1 进位到下一个数字。

实现这一点的函数如下。你可以在 GitHub 上找到所有的代码。 1

 1   function BitwiseAdd(a, b){
 2       while (b != 0) {
 3           var carry = (a & b);
 4           a = a ^ b;
 5           b = carry << 1;
 6       }
 7       return a;
 8   }
 9
10   console.log(BitwiseAdd(4,5)); // 9

这里有两个详细的例子:

bitwiseAdd(4, 5);
4:             0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0
5:             0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1
sum = 4 ^ 5 =  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 = 1 (base 10)
carry = (a & b) << 1
a & b =        0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0
(a & b) << 1 = 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 = 8 (base 10)

bitwiseAdd(1, 8);
1:            0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1
8:            0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0

sum = 1 ^ 8 = 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 = 9 (base 10)

carry =   (a & b) << 1
a & b =   0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
-> return 9 (a)

减法

减法是两个数的差。但是,你也可以把它想成是加一个负数。下面举个例子:5 - 4 = 5 + (-4)

因此,首先使用NOT操作符创建一个求反函数。在二进制中,正二进制数减去负二进制数是通过反转所有位并加 1 得到的。这在下面的代码块中实现:

 1   function BitwiseNegate(a) {
 2       return BitwiseAdd(~a,1);
 3   }
 4
 5   console.log(BitwiseNegate(9)); // -9
 6   // negation with itself gives back original
 7   console.log(BitwiseNegate(BitwiseNegate(9))); // 9
 8
 9   function BitwiseSubtract(a, b) {
10       return BitwiseAdd(a, BitwiseNegate(b));
11   }

12
13   console.log(BitwiseSubtract(5, 4)); // 1

增加

以 2 为基数的数字相乘遵循与以 2 为基数的数字相乘相同的逻辑;将数字相乘,将 10 以上的任何数字进位到下一位,然后将下一位与移位后的基数相乘(对于小数,每次移位都要乘以 10)。例如,12 乘以 24 的方法是先将 2 与 4 相乘,然后将 10 与 4 相乘,然后将数字移动到 2(现在是 20),再将 20 与 2 相乘,然后将 20 乘以 10。最后,将这些值相加得到 288。

    12
    24
------
    48
    24
------
   288

二进制:

         0 1 1 0 0
         1 1 0 0 0
   -----------------
               0 0 0 0 0
            0 0 0 0 0
         0 0 0 0 0
     0 1 1 0 0
  0 1 1 0 0
-------------
  1 0 0 1 0 0 0 0 0

下面的代码块说明了这种实现,它也处理负数:

 1   function BitwiseMultiply(a, b) {
 2       var m = 1,
 3           c = 0;
 4
 5       if (a < 0) {
 6           a = BitwiseNegate(a);
 7           b = BitwiseNegate(b);
 8       }
 9       while (a >= m && b) {
10           if (a & m) {
11               c = BitwiseAdd(b, c);
12           }
13           b = b << 1; 

14           m = m << 1;
15       }
16       return c;
17   }
18   console.log(BitwiseMultiply(4, 5)); // 20

分开

给定a / b,除法可以被认为是你可以从a中减去b的次数。比如 4/2 = 2,因为 4-2-2 = 0。使用此属性,可以按如下方式实现按位除法:

 1   function BitwiseDividePositive(a, b) {
 2       var c = 0;
 3
 4       if (b != 0) {
 5           while (a >= b) {
 6               a = BitwiseSubtract(a, b);
 7               c++;
 8           }
 9       }
10       return c;
11   }
12   console.log(BitwiseDividePositive(10, 2)); // 5

这对于正数来说相对简单。while循环可以一直减法,一个计数器变量可以存储b减去了多少次a。然而,对于负数呢?-10 /2 = -5,但是我们不能从-10 中减去 2,因为while循环会永远继续下去。为了避免这种情况,请将这两个数字都转换为正数。这样做的时候,我们必须跟踪标志。

a        b        a * b
+        +        +
+        -        -
-        +        -
-        -        +

如果负数表示为 1,正数表示为 0,则该表与XOR表相同:

a        b        a * b
0        0        0
0        1        1
1        0        1
1        1        0

除法算法如下所示。该函数从a中减去b,直到为零。同样,负数必须在最后用一个否定辅助函数进行适当的处理。

 1   function BitwiseDivide(a, b) {
 2       var c = 0,
 3           isNegative = 0;
 4
 5       if (a < 0) {
 6           a = BitwiseNegate(a); // convert to positive
 7           isNegative = !isNegative;
 8       }
 9
10       if (b < 0) {
11           b = BitwiseNegate(b); // convert to positive
12           isNegative = !isNegative;
13       }
14
15       if (b != 0) {
16           while (a >= b) {
17               a = BitwiseSubtract(a, b);
18               c++;
19           }
20       }
21
22       if (isNegative) {
23           c = BitwiseNegate(c);
24       }
25
26       return c;
27   }
28
29   console.log(BitwiseDivide(10, 2)); // 5
30   console.log(BitwiseDivide(-10, 2)); // -5

31   console.log(BitwiseDivide(-200, 4)); // -50

摘要

本章讲述了 JavaScript 中位操作的基础知识。位操作用于高性能的数值运算。使用位操作符比在Math类中使用本地方法要快得多。随着 JavaScript 通过 Node.js 进入服务器端编程,需要更高效的代码。为了巩固本章的概念,表 20-1 总结了按位运算符及其用法。

表 20-1

位操作摘要

|

操作员

|

操作

|

用例

| | --- | --- | --- | | & | AND | 两位都为 1 时为 1 | | &#124; | OR | 任一位为 1 时为 1 | | 你是谁 | NOT | 反转所有位 | | ^ | XOR | 1 当只有位之一为 1 时 | | << | 左移 | 向左移位,任何多余的位都被移出 | | >> | 右移 | 向右移位,任何多余的位都被移出 | | >>> | 零填充右移 | 向右移位,任何多余的位被移出,符号位变为 0 |

Footnotes [1](#Fn1_source) [T2`https://github.com/Apress/js-data-structures-and-algorithms`](https://github.com/Apress/js-data-structures-and-algorithms)