JavaScript提供了几种运算符,可以对一些简单的值进行基本操作,比如算术操作、赋值操作、逻辑操作、按位操作等。我们经常可以看到混合了赋值操作,算术操作和逻辑操作的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
下面是一些需要注意的关于32位有符号整数的要点,这些整数由javascript位运算符使用:
以下是一些重要数字的32位序列表示:
0 => 00000000000000000000000000000000
-1 => 11111111111111111111111111111111
2147483647 => 01111111111111111111111111111111
-2147483648 => 10000000000000000000000000000000
从上面的描述可以很容易得出:
~0 => -1
~-1 => 0
~2147483647 => -2147483648
~-2147483648 => 2147483647
大多数JavaScript内置对象(如数组和字符串)都有一些有用的方法,可用于检查数组中是否存在项或字符串中是否存在子字符串。以下是一些方法:
这些方法都返回某一项或子字符串的从零开始的索引(如果找到);否则,它们返回-1。例如:
const numbers = [1, 3, 5, 7, 9];
console.log(numbers.indexOf(5)); // 2
console.log(numbers.indexOf(8)); // -1
如果我们对么某一项或者子字符串的索引位置不感兴趣,我们可以选择使用布尔值。当未找到的项或者子字符串时,返回-1,我们可以认为是false,返回其他的值都是true。
function foundIndex (index) {
return Boolean(~index);
}
在上面的代码片段中,~运算符在-1上使用时的值为0。使用boolean()将值强制转换为boolean,返回false。对于其他每个索引值,返回true。因此,以前的代码段可以修改如下:
const numbers = [1, 3, 5, 7, 9];
console.log(foundIndex(numbers.indexOf(5))); // true
console.log(foundIndex(numbers.indexOf(8))); // false
& 操作符对其操作数的每一对对应位执行一个和运算。& 操作符仅当两个位都为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:
举个例子,假设我们有一个8位的整数,我们希望确保前面的4位被关闭(置为0)。我们可以用&操作符来实现:
const mask = 0b11110000;
// 222 => 11011110
// (222 & mask)
// ------------
// 11011110
// & 11110000
// ------------
// = 11010000
// ------------
// = 208 (decimal)
console.log(222 & mask); // 208
&操作符还有一些其他有用的位屏蔽应用。一个这样的应用是确定给定的位序列是否设置了一个或多个位。例如,假设我们要检查是否为给定的十进制数设置了第五位。以下是我们如何使用&运算符来执行此操作:
首先,创建一个位掩码,用于检查目标位(在本例中为第五位)是否设置为1。位掩码上的每个位都设置为0,但目标位置的位除外,目标位置的位设置为1。二进制数文字可用于轻松实现这一点:
const mask = 0b10000;
接下来,使用十进制数和位掩码作为操作数执行&操作,并将结果与位掩码进行比较。如果所有目标位都设置为十进制数,&操作的结果将等于位掩码。请注意,位掩码中的0位将有效地关闭十进制数中的相应位,因为a&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
运算符对其操作数的每对对应位执行“或”运算。运算符仅当两个位都为0时返回0;否则返回1。
对于一对位,这里是或操作的可能值:
(0 | 0) === 0
(0 | 1) === 1
(1 | 0) === 1
(1 | 1) === 1
在位屏蔽应用中,可以使用运算符来确保位序列中的某些位被打开(设置为1)。这是基于这样一个事实:对于任何给定的位A:
例如,假设我们有一个8位整数,我们希望确保所有偶数位(第二、第四、第六、第八)都打开(设置为1)。| 运算符可用于实现以下目的:
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
^运算符对其操作数的每对对应位执行异或(异或)运算。如果两个位相同(0或1),则^运算符返回0;否则,它返回1。
对于一对位,下面是可能的值:
(0 ^ 0) === 0
(0 ^ 1) === 1
(1 ^ 0) === 1
(1 ^ 1) === 0
在位屏蔽应用程序中,^ 运算符通常用于切换或翻转位序列中的某些位。这是基于这样一个事实:对于任何给定的位A:
(A ^ 0 = A)
(A ^ 1 = 1) — if A is 0
(A ^ 1 = 0) — if A is 1
例如,假设我们有一个8位整数,我们希望确保除了最低有效位(第一位)和最高有效位(第八位)之外,每个位都被切换。可以使用^运算符实现以下目的:
接下来,使用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
从上面列出的标识中可以明显看出,-1上的xor操作等同于a上的按位非操作。因此,上面的foundIndex()函数也可以这样编写:
function foundIndex (index) {
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颜色的每个组件的颜色值在0-255之间。简单地说,每个颜色值可以用8位完美地表示。
0 => 0b00000000 (2进制) => 0x00 (16进制)
255 => 0b11111111 (2进制) => 0xff (16进制)
因此,颜色本身可以完美地用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
// Rearrange the component bits and pad with zeroes as necessary
// Use the left shift operator
(red << 16) => 00000000 11111111 00000000 00000000
(green << 8) => 00000000 00000000 00100011 00000000
(blue) => 00000000 00000000 00000000 00010100
// Combine the component bits together using the OR (|) operator
// ( 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
在总结本教程之前,让我们考虑另一个非常常见的位操作符和位屏蔽应用:配置标志。
假设我们有一个函数,它接受几个布尔选项,这些选项可以用来控制函数的运行方式或返回的值的类型。创建此函数的一种可能方法是将所有选项作为参数传递给该函数,可能使用一些默认值,例如:
function doSomething (optA = true, optB = true, optC = false, optD = true, ...) {
// something happens here...
}
当然,这不太方便。在以下两种情况下,这种方法开始变得相当有问题:
用前面的方法解决问题的一种方法是为配置选项使用一个对象,如下所示:
const defaultOptions = {
optA: true,
optB: true,
optC: false,
optD: true,
...
};
function doSomething (options = defaultOptions) {
// something happens here...
}
这种方法非常优雅,您很可能已经看到它被使用了,甚至自己在某个地方使用过。然而,使用这种方法时,options参数将始终是一个对象,对于配置选项来说,这可以被认为是多余的。
如果所有选项都采用布尔值,则可以使用整数而不是对象来表示选项。在这种情况下,整数的某些位将映射到指定的选项。如果某个位被打开(设置为1),则指定选项的值为“真”;否则为“假”。
我们可以用一个简单的例子来演示这种方法。假设我们有一个函数,它规范化包含数字的数组列表中的项,并返回规范化的数组。返回的数组可以由三个选项控制,即:
我们可以使用一个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)
这里,我们有一个列表“所有”标志,可以激活所有选项。然后,我们使用^运算符停用唯一选项,并根据需要保留其他选项。
现在我们已经准备好了选项标志,可以继续定义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的位操作符有一些非常有趣的用例。我强烈希望您在阅读本文的过程中获得的见解从现在起用在你的日常开发中。
原文标题:Interesting use cases for JavaScript bitwise operators
原文地址:https://blog.logrocket.com/
本文首发于公众号:符合预期的CoyPan
我们都知道通过Math.floor()方法可实现数值的向下取整,得到小于或等于该数字的最大整数。除了Math.floor方法,还可以使用位运算|,>>来实现向下取整哦
扩展运算符( spread )是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。
位运算的方法在其它语言也是一样的,不局限于JS,所以本文提到的位运算也适用于其它语言。位运算是低级的运算操作,所以速度往往也是最快的
平常的数值运算,其本质都是先转换成二进制再进行运算的,而位运算是直接进行二进制运算,所以原则上位运算的执行效率是比较高的,由于位运算的博大精深,下面通过一些在js中使用位运算的实例
js实现:四舍五入、向上取整、向下取整等方法。parseInt、Math.ceil、Math.round、Math.floor、toFixed等的使用
JS经常会遇到延迟执行的动作,并且失败后自动尝试,尝试N次之后就不再尝试的需求,今天刚好又遇到,于是写个闭包,以后不断完善继续复用。检查并计数第一个参数用来标记是尝试哪个动作的,第二个参数是最大尝试次数
大多数语言都提供了按位运算符,恰当的使用按位运算符有时候会取得的很好的效果。在我看来按位运算符应该有7个:& 按位与、| 按位或、^ 按位异或、~ 按位非
PHP取整数函数常用的四种方法:1.直接取整,舍弃小数,保留整数:intval(); 2.四舍五入取整:round(); 3.向上取整,有小数就加1:ceil(); 4.向下取整:floor()。
ECMAScript 中的相等操作符由两个等于号 ( == ) 表示,如果两个操作数相等,则返回 true。相等操作符会先转换操作数(通常称为强制转型),然后比较它们的相等性。
前端工作中经常遇到数字计算保留小数问题,由于不是四舍五入的方式不能使用toFixed函数,本文采用正则表达式匹配字符串的方式,解决对数字的向上或向下保留小数问题:
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!