在平常开发过程当中,你可能听说过位运算符,但是相信大部分人在实际工作中用到位操作的机会也是寥寥无几,而且在一些源码当中,也会经常会碰到类似 !!
、~~
、>>
之类的运算符,所以在本章当中,我们就来深入的探讨一下 JavaScript
当中的位运算符
分类
在 JavaScript
中位运算符一共有七个,如下
运算符 | 用法 | 描述 |
---|---|---|
按位与( AND ) |
a & b |
对于每一个比特位,只有两个操作数相应的比特位都是 1 时,结果才为 1 ,否则为 0 |
按位或(OR ) |
a | b |
对于每一个比特位,当两个操作数相应的比特位至少有一个 1 时,结果为 1 ,否则为 0 |
按位异或(XOR ) |
a ^ b |
对于每一个比特位,当两个操作数相应的比特位有且只有一个 1 时,结果为 1 ,否则为 0 |
按位非(NOT ) |
~ a |
反转操作数的比特位,即 0 变成 1 ,1 变成 0 |
左移(Left shift ) |
a << b |
将 a 的二进制形式向左移 b (< 32) 比特位,右边用 0 填充 |
有符号右移 | a >> b |
将 a 的二进制表示向右移 b (< 32) 位,丢弃被移出的位 |
无符号右移 | a >>> b |
将 a 的二进制表示向右移 b (< 32) 位,丢弃被移出的位,并使用 0 在左侧填充 |
但是在正式展开之前,我们先来看两个平常使用较多的逻辑运算符
!!
这个我们可能经常会遇到,也就是双重否定,通常来说它不是一个独特的 JavaScript
运算符,也不是一个特殊的语法,而只是一个两个否定的序列,它用于任何类型的值转换为它的相应的 true
或 false
取决于它是否是 truthy
或 falsy
布尔值
1 | !!1 // true |
首先否定转换为任意值 false
,然后第二个否定对正常的布尔值进行操作,他们一起可以转换任何 truthy
值为 true
和任何 falsy
值为 false
,但是许多专业人士认为使用这种语法的做法是不可接受的,并建议更简单的阅读替代方案
1 | x !== 0 // instead of !!x in case x is a number |
由于以下原因,使用 !!x
被视为不良做法
- 从风格上看,它可能看起来像一个独特的特殊语法,而实际上除了两个连续的隐式类型转换否定之外,它没有做任何事情
- 最好通过代码提供有关存储在变量和属性中的值类型的信息,例如
x !== 0
表示x
可能是一个数字,而!!x
并没有向代码的读者传达任何这样的优势 Boolean(x)
用法允许类似的功能,并且是类型的更明确的转换
但是在这里我们并不是要去探究使用它的好坏,而是来稍微深入一些,看看它的的运行原理,但是要想了解 !!
的运行原理,我们首先要了解逻辑非运算符 !
,关于逻辑非我们可以参考规范 11.4.9 逻辑非运算符,它是这样定义的
产生式
UnaryExpression : ! UnaryExpression
按照下面的过程执行1、令
expr
为解释执行UnaryExpression
的结果
2、令oldValue
为ToBoolean(GetValue(expr))
3、如果oldValue
为true
,返回false
4、返回true
说白了就是 ToBoolean
返回的 oldValue
为 true
,就为 false
,否则为 true
,理解了逻辑非以后就不难理解 !!
了,!!
的意思就是直接返回 ToBoolean(GetValue(expr))
,就相当于又套了一层,其目的是将操作数转化为布尔类型,相当于 Boolean(value)
,我们来看几个例子加深一下印象
1 | !! 1 // true |
~~
与上面的 !!
类似,我们如果想要理解 ~~
,首先要知道按位非操作符 ~
的概念,通过开头部分的介绍,按位非的意思是反转操作数的比特位,可能不太好理解,换个说法就是对一个二进制位取反,还是先来看规范 11.4.8 按位非运算符,它是这样定义的
产生式
UnaryExpression : ~ UnaryExpression
按照下面的过程执行1、令
expr
为解释执行UnaryExpression
的结果
2、令oldValue
为ToInt32(GetValue(expr))
3、返回oldValue
按位取反的结果,结果为32
位有符号整数
我们来看一个 MDN
上的例子来了解,如下
1 | 9 (base 10) = 00000000000000000000000000001001 (base 2) |
如果不去看什么转换,取反等操作,我们可以简单的理解为,对任一数值 x
进行按位非操作的结果为 -(x + 1)
,例如上面的 ~9
结果为 -10
,如果反过来 ~-10
的结果则为 9
,当然有这个特性的话,也可以使用在 indexOf()
当中
1 | if (~str.indexOf(key)) { |
在明白了按位非 ~
的意思后,我们就再来看看 ~~
,顾名思义,它的作用就是在 ~
的基础上再做一次按位非,等于省略掉定义中的第三步返回 ToInt32(GetValue(expr))
,目的是将操作数转化为 32
位有符号的整数类型,得到结果为 -(-(x + 1) + 1)
,我们来看几个例子加深一下印象
1 | ~~ 0 // 0 |
但是在使用 ~~
进行取整的时候,有一个需要注意的地方,就是注意区分与 parseInt
的区别,两者在本质上是完全不同的
parseInt
的用途是字符串转整数- 位运算符是浮点数转
Int32
(ToInt32
)
但是位运算符和 parseInt
都存在越界问题,其中位运算符限定为 32
位整数,而 parseInt
在转换 number
不能精确表示的数字时会出现问题,parseInt
可能返回 double
类型中的所有整数,也可能返回 NaN
、±Infinity
,而位运算总是返回 Int32
(NaN
和 ±Infinity
会被转为 0
)
1 | // ========== |
在看完了上面的一些示例以后,我们就正式的来看看 JavaScript
中的位运算符
&(按位与)
它的作用是对每一对比特位执行与(&
)操作,其实简单来说就是,如 a & b
,意思就是将 a
和 b
的每个比特位进行 &
运算,即相对应的两个比特位都是 1
时结果为 1
,否则为 0
,将任一数值 x
与 0
执行按位与操作,其结果都为 0
,也就是下表这样
a |
b |
a & b |
---|---|---|
0 |
0 |
0 |
0 |
1 |
0 |
1 |
0 |
0 |
1 |
1 |
1 |
可能直接看上去有点模糊,我们来看下面这个例子
1 | 5 & 6 // 4 |
从结果可知,是为 4
的,那么我们是如何得到 4
这个结果的呢?我们可以先来看看它们两者对应的二进制
5
的二进制为101
6
的二进制为110
然后我们按照上表当中的流程执行后,结果为 100
,转换为十进制后为 4
,所以结果为 4
,下面我们来看一个使用 &
判断奇偶数的小技巧
1 | 4 & 1 // 0 偶数 |
它的原理是,由于数字 1
的二进制是 00000000000000000000000000000001
,并且奇数的二进制最低位也是 1
,所以可以用任意整数和 1
进行 &
运算来判断奇偶
|(按位或)
它的作用是对每一对比特位执行或(|
)操作,如果 a
或 b
为 1
,则 a | b
结果为 1
,也就是下表这样
a |
b |
a | b |
---|---|---|
0 |
0 |
0 |
0 |
1 |
1 |
1 |
0 |
1 |
1 |
1 |
1 |
同样的,我们也通过下面这个例子来进行了解
1 | 5 | 6 // 7 |
从结果可知,是为 7
的,我们在上面已经知道了它们两者对应的二进制是下面这样的
5
的二进制为101
6
的二进制为110
同理按照上表流程执行后,结果为 111
,转换为十进制后为 7
,所以结果为 7
,同样的,我们也来看一个 |
的小技巧,因为位运算只对整数有效,所以在遇到小数时,会将小数部分舍去,只保留整数部分,所以我们可以利用这个特性将一个小数与 0
进行二进制或(|)运算,等同于对该数去除小数部分(向下取整),即只取整数位(需要注意,这种取整方法不适用超过 32
位整数最大值 2147483647
的数)
1 | 3.14159 | 0 // 3 |
下面我们再来看一些示例加深一下印象
1 | 1 | 0 // 1 |
^(按位异或)
异或运算(^
)在两个二进制位不同时返回 1
,相同时返回 0
,如下表
a |
b |
a ^ b |
---|---|---|
0 |
0 |
0 |
0 |
1 |
1 |
1 |
0 |
1 |
1 |
1 |
0 |
同样的,我们还是通过之前的例子来进行了解
1 | 5 ^ 6 // 3 |
在上面我们已经得知它们对应的二进制值,按照上表执行后的结果为 011
,转换为十进制后结果为 3
,同样的,异或运算(^
)也有一个小技巧,当我们连续对两个数 a
和 b
进行三次异或运算 a ^= b
,b ^= a
,a ^= b
后可以互换它们的值,这意味着,使用异或运算(^
)可以在不引入临时变量的前提下,互换两个变量的值
1 | var a = 5 |
同样的,异或运算也可以用来取整
1 | 3.14 ^ 0 // 3 |
<<(左移运算符)
左移运算符(<<
)表示将一个数的二进制值向左移动指定的位数,尾部补 0
,即乘以 2
的指定次方(最高位即符号位不参与移动),所以我们可以得到操作公式如下
1 | x << y = x * Math.pow(2, y) |
比如下面这个 9 << 2
的示例
1 | 9 (base 10): 00000000000000000000000000001001 (base 2) // 转换为二进制原码 |
也就是 9 * Math.pow(2, 2)
,所以结果为 36
, 看完了上面这个 MDN
提供的示例以后,我们再来看一个使用 >>
的小技巧,那就是在使用 >> 1
的时候其实就相当于除以 2
的操作,但是会忽略余数,在某些场景下会比较适用
1 | 11 >> 1 // 5 |
另外左移运算符用于二进制数值非常方便,这也是我们在一些操作颜色等第三方类库中经常可以看到的操作
1 | var color = { r: 186, g: 218, b: 85 } |
最后再来看几个示例加深一下印象
1 | null << 0 // 0 |
>>(有符号右移)
与左移类似,右移运算符(>>
)表示将一个数的二进制值向右移动指定的位数,向右被移出的位被丢弃,拷贝最左侧的位以填充左侧,由于新的最左侧的位总是和以前相同,符号位没有被改变,所以被称作『符号传播』,看下面例子
1 | 9 (base 10): 00000000000000000000000000001001 (base 2) // 转换为二进制原码 |
相比之下,-9 >> 2
得到 -3
,因为符号被保留了
1 | -9 (base 10): 11111111111111111111111111110111 (base 2) |
另外,右移运算符(>>
)可以模拟 2
的整除运算
1 | 5 >> 1 // 2,相当于 5 / 2 = 2 |
也可以和我们上面介绍的异或运算(^
)一起使用来实现取绝对值
1 | function abs(a) { |
同样的,看几个例子加深一下印象
1 | null >> 0 // 0 |
>>>(无符号右移)
该操作符会将第一个操作数向右移动指定的位数,向右被移出的位被丢弃,左侧用 0
填充,它与有符号右移(>>
)的区别就是在于负数的处理不同,因为符号位变成了 0
,所以结果总是非负的,即便右移 0
个比特,结果也是非负的,对于非负数,有符号右移和无符号右移总是返回相同的结果,例如 9 >>> 2
和 9 >> 2
一样返回 2
1 | 9 (base 10): 00000000000000000000000000001001 (base 2) |
而如果是负数,结果却有很大不同
1 | -9 (base 10): 11111111111111111111111111110111 (base 2) |
其实简单来说,例如 length >>> 0
,字面上的意思是指『右移 0
位』,但实际上是把前面的空位用 0
填充,这里的作用是保证结果为数字且为整数,再来看下面几个示例加深一下印象
1 | null >>> 0 // 0 |
一个综合示例
最后的最后,我们再来看一个综合的使用方式,即使用我们之前的介绍的位运算符来实现一个 rgb
值和 16
进制颜色值之间的转换,如下
1 | /** |