二十、位操作
位操作是 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 | 5
和40 | 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 |
| |
| OR
| 任一位为 1 时为 1 |
| 你是谁 | NOT
| 反转所有位 |
| ^
| XOR
| 1 当只有位之一为 1 时 |
| <<
| 左移 | 向左移位,任何多余的位都被移出 |
| >>
| 右移 | 向右移位,任何多余的位都被移出 |
| >>>
| 零填充右移 | 向右移位,任何多余的位被移出,符号位变为 0 |
版权属于:月萌API www.moonapi.com,转载请注明出处