涨姿势!原来JavaScript运算符还可以这么玩
作者 | Glad Chinda译者 | 王强编辑 | Yonie
JavaScript 提供了几种运算符,可以用来对简单的值执行一些基本操作,如算术运算、赋值、逻辑运算、按位运算等。
JavaScript 代码中赋值、算术和逻辑三种运算符经常会混合使用,但按位运算符就用得没那么多了。
JavaScript 按位运算符~ - 按位非(NOT)
& - 按位与(AND)
| - 按位或(OR)
^ - 按位异或(XOR)
<< - 左移
>> - 符号传播(有符号)右移
>>> - 零填充(无符号)右移
这篇教程会逐一介绍 JavaScript 的按位运算符,并总结它们的运算机制。我们还会研究一些实际的 JavaScript 程序案例。这里还要了解一下 JavaScript 按位运算符是怎样将其操作数表示为有符号的 32 位整数的。
按位非(~)~ 运算符是一个一元运算符,只需要一个操作数。~ 运算符对其操作数的每一比特位都执行一次非(NOT)运算。非运算的结果称为反码。一个整数的反码就是整数的每个比特位都反转的结果。
对于给定的整数,比如 170,可以使用~ 运算符计算反码,如下所示:
// 170 => 00000000000000000000000010101010
// --------------------------------------
// ~ 00000000000000000000000010101010
// --------------------------------------
// = 11111111111111111111111101010101
// --------------------------------------
// = -171 (decimal)
console.log(~170); // -171
JavaScript 按位运算符将其操作数转换为补码格式的 32 位有符号整数。因此对整数使用~ 运算符时,结果值是整数的补码。整数 A 的补码由 -(A + 1) 给出。
~170 => -(170 + 1) => -171
关于 JavaScript 按位运算符使用的 32 位有符号整数有几点注意事项:- 最高位(最左边)称为符号位。正整数的符号位始终为 0,负整数的符号位始终为 1。
- 除符号位之外的剩余 31 位用于表示整数。因此可以表示的最大 32 位整数是 (2^32 - 1),即 2147483647,而最小整数是 -(2^31),即 -2147483648。
对于超出 32 位有符号整数范围的整数,最高位将被逐一丢弃,直到整数落在范围内。
以下是一些重要数字的 32 位序列表示:
0 => 00000000000000000000000000000000
-1 => 11111111111111111111111111111111
2147483647 => 01111111111111111111111111111111
-2147483648 => 10000000000000000000000000000000
从上面的表示法中可以看出:
~0 => -1
~-1 => 0
~2147483647 => -2147483648
~-2147483648 => 2147483647
找到的索引大多数 JavaScript 内置对象(如数组和字符串)都带有一些方法,可用于查找数组中的项目或字符串中的子字符串。下面是其中一些方法:Array.indexOf()
Array.lastIndexOf()
Array.findIndex()
String.indexOf()
String.lastIndexOf()
String.search()
这些方法都返回项目或子字符串的从零开始的索引(如果找得到);否则会返回 -1。例如:
const numbers = [1, 3, 5, 7, 9];
console.log(numbers.indexOf(5)); // 2
console.log(numbers.indexOf(8)); // -1
如果我们对找到的项或子字符串的索引不感兴趣,则可以使用布尔值,这样未找到项目或子字符串时返回 false 来代替 -1,而找到其他值时返回 true。代码如下:
function foundIndex (index) {
return Boolean(~index);
}
在上面的代码中,~ 运算符在 -1 上使用时计算结果为 0,这是一个虚值。因此使用 Boolean() 将虚值转换为布尔值,返回 false。对于其他索引值则返回 true。因此上面的代码段可以修改如下:
const numbers = [1, 3, 5, 7, 9];
console.log(foundIndex(numbers.indexOf(5))); // true
console.log(foundIndex(numbers.indexOf(8))); // false
按位与(&)& 运算符对其操作数的每对比特位执行与(AND)运算。仅当两个位都为 1 时,& 运算符才返回 1;否则它返回 0。因此,与操作的结果相当于将每对比特位相乘。
对于一对比特位来说,与运算可能的值如下所示。
(0 & 0) === 0 // 0 x 0 = 0
(0 & 1) === 0 // 0 x 1 = 0
(1 & 0) === 0 // 1 x 0 = 0
(1 & 1) === 1 // 1 x 1 = 1
关闭位& 运算符在位掩码应用中通常用来对特定的位序列关闭某些位。因为对于任意给定的位 A 来说:(A&0 = 0) - 对应的位是 0 时,该位关闭。
(A&1 = A) - 对应的位是 1 时,该位不变。
例如我们有一个 8 位整数,我们希望关闭右数前 4 位(设置为 0),就可以用 & 运算符实现此目的:
首先创建一个位掩码,其效果是关闭 8 位整数的右数前 4 位。这个位掩码为 0b11110000。注意位掩码的右数前 4 位设置为 0,其他位设置为 1。
接下来对 8 位整数和刚创建的位掩码执行 & 操作:
const mask = 0b11110000;
// 222 => 11011110
// (222 & mask)
// ------------
// 11011110
// & 11110000
// ------------
// = 11010000
// ------------
// = 208 (decimal)
console.log(222 & mask); // 208
检查设置位& 运算符还有其他一些位掩码用途。一种用途是检查给定的位序列是否设置了一个或多个位。例如我们要检查右数第五个比特位是否设置为某个十进制数字,就可以使用 & 运算符来执行此操作:首先创建一个位掩码,用于检查目标位(这里是第五位)是否设置为 1。位掩码上的其他位都设置为 0,但目标位设置为 1。二进制数字字面量如下:
const mask = 0b10000;
接下来,使用十进制数和位掩码作为操作数执行 & 操作,并将结果与位掩码对比。如果所有目标位都设置为这个十进制数,则 & 操作的结果将等于位掩码。注意,由于 A&0 = 0,位掩码中的 0 位将关闭十进制数中的对应位。
// 34 => 100010
// (34 & mask) => (100010 & 010000) = 000000
console.log((34 & mask) === mask); // false
// 50 => 110010
// (50 & mask) => (110010 & 010000) = 010000
console.log((50 & mask) === mask); // true
偶数或奇数进一步扩展上面的用途,可以用 & 运算符检查给定的十进制数是偶数还是奇数。这里的位掩码是 1(以确定最右位是否设置过了)。
对于整数来说,可以使用最低位(最右位)来确定该数字是偶数还是奇数。如果最低位被打开了(设置为 1),则该数字为奇数;否则就是偶数。
function isOdd (int) {
return (int & 1) === 1;
}
function isEven (int) {
return (int & 1) === 0;
}
console.log(isOdd(34)); // false
console.log(isOdd(-63)); // true
console.log(isEven(-12)); // true
console.log(isEven(199)); // false
实用等式以下是一些 & 运算的实用等式(对于任何有符号的 32 位整数 A 来说):
(A & 0) === 0
(A & ~A) === 0
(A & A) === A
(A & -1) === A
按位或(|)| 运算符对其操作数的每对比特位执行或(OR)运算。只有当两个位都为 0 时|运算符才返回 0;否则它返回 1。
对于一对比特位来说,或运算的可能结果如下:
(0 | 0) === 0
(0 | 1) === 1
(1 | 0) === 1
(1 | 1) === 1
打开位针对位掩码用途,| 运算符可用来打开一个位序列中的某些位(设置为 1)。因为对于任意给定的位 A 来说:(A | 0 = A) - 对应的位是 0 时,该位保持不变。
(A | 1 = 1) - 对应的位是 1 时,该位打开。
首先创建一个位掩码,其效果是打开 8 位整数的每个偶数位。这个位掩码是 0b10101010。注意位掩码的偶数位设置为 1,其他位设置为 0。
接下来对 8 位整数和刚创建的位掩码执行| 操作:
const mask = 0b10101010;
// 208 => 11010000
// (208 | mask)
// ------------
// 11010000
// | 10101010
// ------------
// = 11111010
// ------------
// = 250 (decimal)
console.log(208 | mask); // 250
实用等式以下是一些|的实用等式(对于任何有符号的 32 位整数 A 来说):
(A | 0) === A
(A | ~A) === -1
(A | A) === A
(A | -1) === -1
按位异或(^)^ 运算符对其操作数的每对比特位执行异或(XOR)运算。如果两个位相同(0 或 1),^ 运算符返回 0;否则它返回 1。
对于一对比特位来说,异或运算的可能结果如下。
(0 ^ 0) === 0
(0 ^ 1) === 1
(1 ^ 0) === 1
(1 ^ 1) === 0
翻转位针对位掩码用途,^ 运算符通常用于翻转位序列中的某些位。因为对于任何指定的位 A 来说:其对应的位是 0 时,该位保持不变。(A ^ 0 = A)
-
对应的位是 1 时,该位始终翻转。
(A ^ 1 = 1) - 如果 A 为 0(A ^ 1 = 0) - 如果 A 是 1
首先创建一个位掩码,其效果是翻转 8 位整数除最低位和最高位之外的每个位。这个位掩码是 0b01111110。注意,要翻转的位设置为 1,其他位设置为 0。
接下来对 8 位整数和刚创建的位掩码执行 ^ 操作:
const mask = 0b01111110;
// 208 => 11010000
// (208 ^ mask)
// ------------
// 11010000
// ^ 01111110
// ------------
// = 10101110
// ------------
// = 174 (decimal)
console.log(208 ^ mask); // 174
实用等式下面是一些 ^ 运算的实用等式(对于任何有符号的 32 位整数 A 来说):
(A ^ 0) === A
(A ^ ~A) === -1
(A ^ A) === 0
(A ^ -1) === ~A
如上所示,对 A 和 -1 执行异或运算相当于对 A 执行非运算。因此之前的 foundIndex() 函数也可以这样写:
function foundIndex (index) {
return Boolean(index ^ -1);
}
左移(<<)< span="">左移(<<)运算符需要两个操作数。第一个操作数是整数,而第二个操作数是第一个操作数要向左移位的位数。右面空出来的位用 0 填充,左边移出去的位会被丢弃。
例如对整数 170 来说,假设我们想要向左移三位,对其使用<<运算符如下所示:
// 170 => 00000000000000000000000010101010
// 170 << 3
// --------------------------------------------
// (000)00000000000000000000010101010(***)
// --------------------------------------------
// = (***)00000000000000000000010101010(000)
// --------------------------------------------
// = 00000000000000000000010101010000
// --------------------------------------------
// = 1360 (decimal)
console.log(170 << 3); // 1360
可以使用以下 JavaScript 表达式定义左移位运算符(<<):
(A << B) => A * (2 ** B) => A * Math.pow(2, B)
套用前面的例子:
(170 << 3) => 170 * (2 ** 3) => 170 * 8 => 1360
颜色转换:RGB 到十六进制左移(<<)运算符的一个常见用途是将颜色从 RGB 表示转换为十六进制表示。
RGB 颜色的每个分量的值在 0 到 255 之间。简单来说,每个颜色值刚好能用 8 位表示。
0 => 0b00000000 (binary) => 0x00 (hexadecimal)
255 => 0b11111111 (binary) => 0xff (hexadecimal)
因此颜色可以用 24 位(红色,绿色和蓝色各 8 位)完美表示。右数前 8 位表示蓝色分量,接下来的 8 位表示绿色分量,最后的 8 位表示红色分量。
(binary) => 11111111 00100011 00010100
(red) => 11111111 => ff => 255
(green) => 00100011 => 23 => 35
(blue) => 00010100 => 14 => 20
(hex) => ff2314
现在我们已经知道如何将颜色表示为 24 位序列了,下面探讨如何用各个分量值组成 24 位颜色。假设我们有一个由 rgb(255,35,20) 表示的颜色。下面是组合方法:
(red) => 255 => 00000000 00000000 00000000 11111111
(green) => 35 => 00000000 00000000 00000000 00100011
(blue) => 20 => 00000000 00000000 00000000 00010100
// 重新排列位,补上必要的 0
// 使用左移运算符
(red << 16) => 00000000 11111111 00000000 00000000
(green << 8) => 00000000 00000000 00100011 00000000
(blue) => 00000000 00000000 00000000 00010100
// 使用或 (|) 运算符组合起来
// ( red << 16 | green << 8 | blue )
00000000 11111111 00000000 00000000
| 00000000 00000000 00100011 00000000
| 00000000 00000000 00000000 00010100
// -----------------------------------------
00000000 11111111 00100011 00010100
// -----------------------------------------
这样流程就很清楚了。这里用了一个简单的函数,其将颜色的 RGB 值作为输入数组,并根据上述过程返回对应的十六进制颜色表示:
function rgbToHex ([red = 0, green = 0, blue = 0] = []) {
return `#${(red << 16 | green << 8 | blue).toString(16)}`;
}
符号传播右移(>>)符号传播右移(>>)运算符需要两个操作数。第一个操作数是一个整数,而第二个操作数是第一个操作数需要向右移的位数。
向右移多出来的位会被丢弃,左边空出来的位用符号位(最左位)的副本填充。结果整数的符号不变。这就是这个运算符名称的来历。
例如给定整数 170 和 -170。假设我们想要向右移三位。我们可以使用>>运算符操作如下:
// 170 => 00000000000000000000000010101010
// -170 => 11111111111111111111111101010110
// 170 >> 3
// --------------------------------------------
// (***)00000000000000000000000010101(010)
// --------------------------------------------
// = (000)00000000000000000000000010101(***)
// --------------------------------------------
// = 00000000000000000000000000010101
// --------------------------------------------
// = 21 (decimal)
// -170 >> 3
// --------------------------------------------
// (***)11111111111111111111111101010(110)
// --------------------------------------------
// = (111)11111111111111111111111101010(***)
// --------------------------------------------
// = 11111111111111111111111111101010
// --------------------------------------------
// = -22 (decimal)
console.log(170 >> 3); // 21
console.log(-170 >> 3); // -22
符号传播右移位运算符(>>)可以通过以下 JavaScript 表达式来描述:
(A >> B) => Math.floor(A / (2 ** B)) => Math.floor(A / Math.pow(2, B)
套回前面的例子:
(170 >> 3) => Math.floor(170 / (2 ** 3)) => Math.floor(170 / 8) => 21
(-170 >> 3) => Math.floor(-170 / (2 ** 3)) => Math.floor(-170 / 8) => -22
颜色提取右移(>>)运算符的一个常见用途是从颜色中提取 RGB 颜色值。当颜色以 RGB 表示时很容易区分红色、绿色和蓝色分量值。但对于十六进制的颜色表示来说就没那么直观了。
在上一节中,我们知道了从各个分量(红色、绿色和蓝色)组成颜色是怎样的过程。这个过程倒过来就能用来提取颜色的各个分量的值。下面来试一试。
假设我们有一个由十六进制表示为 #ff2314 的颜色。以下是这个颜色的有符号 32 位表示:
(color) => ff2314 (hexadecimal) => 11111111 00100011 00010100 (binary)
// 32-bit representation of color
00000000 11111111 00100011 00010100
为了获得各个分量,我们将颜色位右移 8 的倍数,直到右数前 8 位是我们需要的分量为止。由于 32 位颜色的最高位是 0,我们可以安全地使用符号传播右移(>>)运算符。
color => 00000000 11111111 00100011 00010100
// Right shift the color bits by multiples of 8
// Until the target component bits are the first 8 bits from the right
red => color >> 16
=> 00000000 11111111 00100011 00010100 >> 16
=> 00000000 00000000 00000000 11111111
green => color >> 8
=> 00000000 11111111 00100011 00010100 >> 8
=> 00000000 00000000 11111111 00100011
blue => color >> 0 => color
=> 00000000 11111111 00100011 00010100
现在右起前 8 位就是我们的目标分量,我们需要一种方法来屏蔽掉前 8 位之外的位数。这里又要使用与(&)运算符。记住 & 运算符可用于关闭某些位。
先来创建所需的位掩码。如下所示:
mask => 00000000 00000000 00000000 11111111
=> 0b11111111 (binary)
=> 0xff (hexadecimal)
位掩码准备就绪后,我们可以用它对先前右移操作的各个结果执行与(&)运算,以提取目标分量。
red => color >> 16 & 0xff
=> 00000000 00000000 00000000 11111111
=> & 00000000 00000000 00000000 11111111
=> = 00000000 00000000 00000000 11111111
=> 255 (decimal)
green => color >> 8 & 0xff
=> 00000000 00000000 11111111 00100011
=> & 00000000 00000000 00000000 11111111
=> = 00000000 00000000 00000000 00100011
=> 35 (decimal)
blue => color & 0xff
=> 00000000 11111111 00100011 00010100
=> & 00000000 00000000 00000000 11111111
=> = 00000000 00000000 00000000 00010100
=> 20 (decimal)
如上所示。这是一个简单的函数,它将十六进制颜色字符串(带有六个十六进制数字)作为输入,并返回对应的 RGB 颜色分量值数组。
function hexToRgb (hex) {
hex = hex.replace(/^#?([0-9a-f]{6})$/i, '$1');
hex = Number(`0x${hex}`);
return [
hex >> 16 & 0xff, // red
hex >> 8 & 0xff, // green
hex & 0xff // blue
];
}
零填充右移(>>>)零填充右移(>>>)运算符的行为很像符号传播右移(>>)运算符。它们的关键区别在于左边填充的位数。
顾名思义,这里左边空出来的位都是用 0 填充的。因此>>>运算符始终返回无符号的 32 位整数,因为结果的符号位始终为 0。对于正整数来说,>>和>>>将始终返回相同的结果。
例如对于整数 170 和 -170 来说,假设我们要向右移 3 位,可以使用>>>运算符操作如下:
// 170 => 00000000000000000000000010101010
// -170 => 11111111111111111111111101010110
// 170 >>> 3
// --------------------------------------------
// (***)00000000000000000000000010101(010)
// --------------------------------------------
// = (000)00000000000000000000000010101(***)
// --------------------------------------------
// = 00000000000000000000000000010101
// --------------------------------------------
// = 21 (decimal)
// -170 >>> 3
// --------------------------------------------
// (***)11111111111111111111111101010(110)
// --------------------------------------------
// = (000)11111111111111111111111101010(***)
// --------------------------------------------
// = 00011111111111111111111111101010
// --------------------------------------------
// = 536870890 (decimal)
console.log(170 >>> 3); // 21
console.log(-170 >>> 3); // 536870890
配置标志最后讨论另一个非常常见的按位运算符和位掩码应用:配置标志(flag)。
假设我们有一个函数,其接受一些 boolean 选项,这些选项可用于控制函数的运行方式或返回的值类型。一种方法是将所有选项作为参数传递给函数,可能还有一些默认值,如下所示:
function doSomething (optA = true, optB = true, optC = false, optD = true, ...) {
// 这个函数做一些事情……
}
当然这样不太方便。下面两种情况下这种方法会变得非常复杂:
假如有超过 10 个布尔选项。我们无法使用那么多参数定义我们的函数。
假如我们只想为第五个和第九个选项指定新的值,其他选项则保留其默认值。此时我们需要调用该函数,对其他选项的参数传递默认值,对第五个和第九个选项则传递指定的值。
解决这个问题的一种方法是使用一个对象作为配置选项,如下所示:
const defaultOptions = {
optA: true,
optB: true,
optC: false,
optD: true,
...
};
function doSomething (options = defaultOptions) {
// 做一些事情……
}
这种方法非常优雅,最为常见。但使用这种方法时 options 参数始终是一个对象,如果只是用来配置选项的话未免杀鸡用牛刀了。
如果所有选项都采用布尔值,我们可以使用整数而不是对象来表示选项。在这种情况下,整数的特定位将映射到指定的选项。如果某个位被打开(设置为 1),则指定选项的值为 true;否则为 false。
举一个简单的例子。假设我们有一个函数来规范化包含数字的数组列表的项目并返回规范化数组。返回的数组可以通过三个选项控制,即:
fraction:将数组中的每个项目除以数组中的最大项目。
unique:从数组中删除重复的项目。
sorted:从最低到最高排序数组的项目。
我们可以使用 3 位整数来表示这些选项,每个位都映射到一个选项。下面的代码是选项标志:
const LIST_FRACTION = 0x1; // (001)
const LIST_UNIQUE = 0x2; // (010)
const LIST_SORTED = 0x4; // (100)
要激活一个或多个选项时,可以使用| 运算符根据需要组合对应的标志。例如我们可以创建一个激活所有选项的标志,如下所示:
const LIST_ALL = LIST_FRACTION | LIST_UNIQUE | LIST_SORTED; // (111)
假设我们只希望默认激活 fraction 和 sorted 选项。我们可以使用| 运算符操作如下:
const LIST_DEFAULT = LIST_FRACTION | LIST_SORTED; // (101)
只有三个选项时看起来还不错,但选项变多时就会变得乱七八糟,还需要激活很多默认选项。在这种情况下,更好的方法是使用 ^ 运算符停用不需要的选项:
const LIST_DEFAULT = LIST_ALL ^ LIST_UNIQUE; // (101)
这里我们用 LIST_ALL 标志来激活所有选项。然后我们使用 ^ 运算符停用某些选项,其他选项则保持激活状态。
现在我们已经准备好了选项标志,就可以继续定义 normalizeList() 函数:
function normalizeList (list, flag = LIST_DEFAULT) {
if (flag & LIST_FRACTION) {
const max = Math.max(...list);
list = list.map(value => Number((value / max).toFixed(2)));
}
if (flag & LIST_UNIQUE) {
list = [...new Set(list)];
}
if (flag & LIST_SORTED) {
list = list.sort((a, b) => a - b);
}
return list;
}
要检查选项是否已激活,我们使用 & 运算符检查选项的对应位是否已打开(设置为 1)。& 操作是使用传递给函数的 flag 参数和选项的对应标志来执行的,如下所示:
// Checking if the unique option is activated
// (flag & LIST_UNIQUE) === LIST_UNIQUE (activated)
// (flag & LIST_UNIQUE) === 0 (deactivated)
flag & LIST_UNIQUE
小结文章比较长,读起来有些枯燥,恭喜你坚持看完了。
你也看到了,虽然 JavaScript 按位运算符用得不多,但它有一些非常有趣的用例。希望大家能在实际编程工作中用到本文学到的内容。
编程快乐!
英文原文: https://blog.logrocket.com/interesting-use-cases-for-javascript-bitwise-operators/
活动推荐前端团队能力如何发展,才能在研发组织中体现更多价值?新框架千千万,前端 leader 要如何为组织做选择?极客时间提供前端团队基础技能培养和知识拓展所需系列课程,帮助前端工程师强化岗位知识结构,提升问题解决能力和创新能力。团队中不同技能水平的开发者能在极客时间平台上共同学习,根据个人基础选择课程;leader 随时掌握团队学习进度,激励团队成长。详细了解团队学习模式,请扫描图上二维码,或点击「阅读原文」。