做者 | Glad Chinda译者 | 王强编辑 | Yoniejavascript
JavaScript 提供了几种运算符,能够用来对简单的值执行一些基本操做,如算术运算、赋值、逻辑运算、按位运算等。前端
JavaScript 代码中赋值、算术和逻辑三种运算符常常会混合使用,但按位运算符就用得没那么多了。java
JavaScript 按位运算符~ - 按位非(NOT)编程
& - 按位与(AND)数组
| - 按位或(OR)安全
^ - 按位异或(XOR)前端工程师
<< - 左移框架
>> - 符号传播(有符号)右移ide
>>> - 零填充(无符号)右移函数
这篇教程会逐一介绍 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 位有符号整数有几点注意事项:
对于超出 32 位有符号整数范围的整数,最高位将被逐一丢弃,直到整数落在范围内。
如下是一些重要数字的 32 位序列表示:
0 => 00000000000000000000000000000000
-1 => 11111111111111111111111111111111
2147483647 => 01111111111111111111111111111111
-2147483648 => 10000000000000000000000000000000
从上面的表示法中能够看出:
~0 => -1找到的索引大多数 JavaScript 内置对象(如数组和字符串)都带有一些方法,可用于查找数组中的项目或字符串中的子字符串。下面是其中一些方法:
~-1 => 0
~2147483647 => -2147483648
~-2147483648 => 2147483647
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关闭位& 运算符在位掩码应用中一般用来对特定的位序列关闭某些位。由于对于任意给定的位 A 来讲:
(0 & 1) === 0 // 0 x 1 = 0
(1 & 0) === 0 // 1 x 0 = 0
(1 & 1) === 1 // 1 x 1 = 1
(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打开位针对位掩码用途,| 运算符可用来打开一个位序列中的某些位(设置为 1)。由于对于任意给定的位 A 来讲:
(0 | 1) === 1
(1 | 0) === 1
(1 | 1) === 1
(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翻转位针对位掩码用途,^ 运算符一般用于翻转位序列中的某些位。由于对于任何指定的位 A 来讲:
(0 ^ 1) === 1
(1 ^ 0) === 1
(1 ^ 1) === 0
其对应的位是 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) {左移(<<)< span="">
return Boolean(index ^ -1);
}
左移(<<)运算符须要两个操做数。第一个操做数是整数,而第二个操做数是第一个操做数要向左移位的位数。右面空出来的位用 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 随时掌握团队学习进度,激励团队成长。详细了解团队学习模式,请扫描图上二维码,或点击「阅读原文」。