JavaScript 中的位运算符

JavaScript 中的位运算符

在平常开发过程当中,你可能听说过位运算符,但是相信大部分人在实际工作中用到位操作的机会也是寥寥无几,而且在一些源码当中,也会经常会碰到类似 !!~~>> 之类的运算符,所以在本章当中,我们就来深入的探讨一下 JavaScript 当中的位运算符

分类

JavaScript 中位运算符一共有七个,如下

运算符 用法 描述
按位与( AND a & b 对于每一个比特位,只有两个操作数相应的比特位都是 1 时,结果才为 1,否则为 0
按位或(OR a | b 对于每一个比特位,当两个操作数相应的比特位至少有一个 1 时,结果为 1,否则为 0
按位异或(XOR a ^ b 对于每一个比特位,当两个操作数相应的比特位有且只有一个 1 时,结果为 1,否则为 0
按位非(NOT ~ a 反转操作数的比特位,即 0 变成 11 变成 0
左移(Left shift a << b a 的二进制形式向左移 b (< 32) 比特位,右边用 0 填充
有符号右移 a >> b a 的二进制表示向右移 b (< 32) 位,丢弃被移出的位
无符号右移 a >>> b a 的二进制表示向右移 b (< 32) 位,丢弃被移出的位,并使用 0 在左侧填充

但是在正式展开之前,我们先来看两个平常使用较多的逻辑运算符

!!

这个我们可能经常会遇到,也就是双重否定,通常来说它不是一个独特的 JavaScript 运算符,也不是一个特殊的语法,而只是一个两个否定的序列,它用于任何类型的值转换为它的相应的 truefalse 取决于它是否是 truthyfalsy 布尔值

1
2
3
4
5
!!1          // true
!!0 // false
!!undefined // false
!!{} // true
!![] // true

首先否定转换为任意值 false,然后第二个否定对正常的布尔值进行操作,他们一起可以转换任何 truthy 值为 true 和任何 falsy 值为 false,但是许多专业人士认为使用这种语法的做法是不可接受的,并建议更简单的阅读替代方案

1
2
x !== 0      // instead of !!x in case x is a number
x != null // instead of !!x in case x is an object, a string, or an undefined

由于以下原因,使用 !!x 被视为不良做法

  • 从风格上看,它可能看起来像一个独特的特殊语法,而实际上除了两个连续的隐式类型转换否定之外,它没有做任何事情
  • 最好通过代码提供有关存储在变量和属性中的值类型的信息,例如 x !== 0 表示 x 可能是一个数字,而 !!x 并没有向代码的读者传达任何这样的优势
  • Boolean(x) 用法允许类似的功能,并且是类型的更明确的转换

但是在这里我们并不是要去探究使用它的好坏,而是来稍微深入一些,看看它的的运行原理,但是要想了解 !! 的运行原理,我们首先要了解逻辑非运算符 ! ,关于逻辑非我们可以参考规范 11.4.9 逻辑非运算符,它是这样定义的

产生式 UnaryExpression : ! UnaryExpression 按照下面的过程执行

1、令 expr 为解释执行 UnaryExpression 的结果
2、令 oldValueToBoolean(GetValue(expr))
3、如果 oldValuetrue,返回 false
4、返回 true

说白了就是 ToBoolean 返回的 oldValuetrue,就为 false,否则为 true,理解了逻辑非以后就不难理解 !! 了,!! 的意思就是直接返回 ToBoolean(GetValue(expr)),就相当于又套了一层,其目的是将操作数转化为布尔类型,相当于 Boolean(value),我们来看几个例子加深一下印象

1
2
3
4
5
6
7
8
!! 1          // true
!! 0 // false
!! null // false
!! undefined // false
!! NaN // false
!! '' // false
!! 'abc' // true
!! 100 // true

~~

与上面的 !! 类似,我们如果想要理解 ~~,首先要知道按位非操作符 ~ 的概念,通过开头部分的介绍,按位非的意思是反转操作数的比特位,可能不太好理解,换个说法就是对一个二进制位取反,还是先来看规范 11.4.8 按位非运算符,它是这样定义的

产生式 UnaryExpression : ~ UnaryExpression 按照下面的过程执行

1、令 expr 为解释执行 UnaryExpression 的结果
2、令 oldValueToInt32(GetValue(expr))
3、返回 oldValue 按位取反的结果,结果为 32 位有符号整数

我们来看一个 MDN 上的例子来了解,如下

1
2
3
 9 (base 10) = 00000000000000000000000000001001 (base 2)
--------------------------------
~9 (base 10) = 11111111111111111111111111110110 (base 2) = -10 (base 10)

如果不去看什么转换,取反等操作,我们可以简单的理解为,对任一数值 x 进行按位非操作的结果为 -(x + 1),例如上面的 ~9 结果为 -10,如果反过来 ~-10 的结果则为 9,当然有这个特性的话,也可以使用在 indexOf() 当中

1
2
3
4
5
if (~str.indexOf(key)) {
// key 包含在字符串 str 中
} else {
// key 不包含在字符串 str 中
}

在明白了按位非 ~ 的意思后,我们就再来看看 ~~,顾名思义,它的作用就是在 ~ 的基础上再做一次按位非,等于省略掉定义中的第三步返回 ToInt32(GetValue(expr)),目的是将操作数转化为 32 位有符号的整数类型,得到结果为 -(-(x + 1) + 1),我们来看几个例子加深一下印象

1
2
3
4
5
6
7
8
9
10
11
~~ 0          // 0
~~ 1 // 1
~~ null // 0
~~ undefined // 0
~~ NaN // 0
~~ '' // 0
~~ 'abc' // 1
~~ 1.4 // 1
~~ -1.4 // -1
~~ 1.5 // 1
~~ -1.5 // -1

但是在使用 ~~ 进行取整的时候,有一个需要注意的地方,就是注意区分与 parseInt 的区别,两者在本质上是完全不同的

  • parseInt 的用途是字符串转整数
  • 位运算符是浮点数转 Int32ToInt32

但是位运算符和 parseInt 都存在越界问题,其中位运算符限定为 32 位整数,而 parseInt 在转换 number 不能精确表示的数字时会出现问题,parseInt 可能返回 double 类型中的所有整数,也可能返回 NaN±Infinity,而位运算总是返回 Int32NaN±Infinity 会被转为 0

1
2
3
4
5
6
7
8
// ==========
console.log(parseInt(1e21)) // 1
console.log(parseInt(2e21)) // 2
console.log(parseInt('abc')) // NaN

console.log(~~'abc') // 0
console.log(~~999999999.909) // 999999999
console.log(~~9999999999.909) // 1410065407

在看完了上面的一些示例以后,我们就正式的来看看 JavaScript 中的位运算符

&(按位与)

它的作用是对每一对比特位执行与(&)操作,其实简单来说就是,如 a & b,意思就是将 ab 的每个比特位进行 & 运算,即相对应的两个比特位都是 1 时结果为 1,否则为 0,将任一数值 x0 执行按位与操作,其结果都为 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
2
4 & 1  // 0 偶数
5 & 1 // 1 奇数

它的原理是,由于数字 1 的二进制是 00000000000000000000000000000001,并且奇数的二进制最低位也是 1,所以可以用任意整数和 1 进行 & 运算来判断奇偶

|(按位或)

它的作用是对每一对比特位执行或(|)操作,如果 ab1,则 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
2
3.14159 | 0     // 3
-3.14159 | 0 // -3

下面我们再来看一些示例加深一下印象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1 | 0           // 1
1.1 | 0 // 1

'asd' | 0 // 0
'123456' | 0 // 123456
0 | 0 // 0

(-1) | 0 // -1
(-1.5646) | 0 // -1

[] | 0 // 0
({}) | 0 // 0

1.23E2 | 0 // 123
-1.23E2 | 0 // -123
1.23E12 | 0 // 1639353344
-1.23E12 | 0 // -1639353344

^(按位异或)

异或运算(^)在两个二进制位不同时返回 1,相同时返回 0,如下表

a b a ^ b
0 0 0
0 1 1
1 0 1
1 1 0

同样的,我们还是通过之前的例子来进行了解

1
5 ^ 6     // 3

在上面我们已经得知它们对应的二进制值,按照上表执行后的结果为 011,转换为十进制后结果为 3,同样的,异或运算(^)也有一个小技巧,当我们连续对两个数 ab 进行三次异或运算 a ^= bb ^= aa ^= b 后可以互换它们的值,这意味着,使用异或运算(^)可以在不引入临时变量的前提下,互换两个变量的值

1
2
3
4
5
6
7
var a = 5
var b = 6

a ^= b, b ^= a, a ^= b

a // 6
b // 5

同样的,异或运算也可以用来取整

1
3.14 ^ 0  // 3

<<(左移运算符)

左移运算符(<<)表示将一个数的二进制值向左移动指定的位数,尾部补 0,即乘以 2 的指定次方(最高位即符号位不参与移动),所以我们可以得到操作公式如下

1
x << y = x * Math.pow(2, y)

比如下面这个 9 << 2 的示例

1
2
3
     9 (base 10): 00000000000000000000000000001001 (base 2)                 // 转换为二进制原码
--------------------------------
9 << 2 (base 10): 00000000000000000000000000100100 (base 2) = 36 (base 10) // 向左移动两位,丢弃左侧两位,右侧补 0 得到二进制

也就是 9 * Math.pow(2, 2),所以结果为 36, 看完了上面这个 MDN 提供的示例以后,我们再来看一个使用 >> 的小技巧,那就是在使用 >> 1 的时候其实就相当于除以 2 的操作,但是会忽略余数,在某些场景下会比较适用

1
2
11 >> 1  // 5
23 >> 1 // 11

另外左移运算符用于二进制数值非常方便,这也是我们在一些操作颜色等第三方类库中经常可以看到的操作

1
2
3
4
5
6
7
8
9
10
var color = { r: 186, g: 218, b: 85 }

// RGB to HEX,(1 << 24) 的作用为保证结果是 6 位数
var rgb2hex = function (r, g, b) {
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b)
.toString(16) // 先转成十六进制,然后返回字符串
.substr(1) // 去除字符串的最高位,返回后面六个字符串
}

rgb2hex(color.r, color.g, color.b) // '#bada55'

最后再来看几个示例加深一下印象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
null << 0       // 0
undefined << 0 // 0
NaN << 0 // 0

12345 << 0 // 12345
-12345 << 0 // -12345
30.12 << 0 // 30
30.54 << 0 // 30

1 << 1 // 2
1 << 2 // 4
-1 << 1 // -2

'12345' << 0 // 12345
'' << 0 // 0
'abc' << 0 // 0

>>(有符号右移)

与左移类似,右移运算符(>>)表示将一个数的二进制值向右移动指定的位数,向右被移出的位被丢弃,拷贝最左侧的位以填充左侧,由于新的最左侧的位总是和以前相同,符号位没有被改变,所以被称作『符号传播』,看下面例子

1
2
3
     9 (base 10): 00000000000000000000000000001001 (base 2)                // 转换为二进制原码
--------------------------------
9 >> 2 (base 10): 00000000000000000000000000000010 (base 2) = 2 (base 10) // 向右移两位,左侧补两位符号位 00

相比之下,-9 >> 2 得到 -3,因为符号被保留了

1
2
3
     -9 (base 10): 11111111111111111111111111110111 (base 2)
--------------------------------
-9 >> 2 (base 10): 11111111111111111111111111111101 (base 2) = -3 (base 10)

另外,右移运算符(>>)可以模拟 2 的整除运算

1
2
3
4
5
6
7
5 >> 1   // 2,相当于 5 / 2 = 2

21 >> 2 // 5,相当于 21 / 4 = 5

21 >> 3 // 2,相当于 21 / 8 = 2

21 >> 4 // 1,相当于 21 / 16 = 1

也可以和我们上面介绍的异或运算(^)一起使用来实现取绝对值

1
2
3
4
5
function abs(a) {
// 如果 a 为正数或 0 则 b 为 0,如果 a 是负数则 b 为 -1
var b = a >> 31
return (a ^ b) - b
}

同样的,看几个例子加深一下印象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
null >> 0       // 0
undefined >> 0 // 0
NaN >> 0 // 0

12345 >> 0 // 12345
-12345 >> 0 // -12345
30.12 >> 0 // 30
30.54 >> 0 // 30
1 >> 1 // 0
-1 << 1 // -1

'12345' >> 0 // 12345
'' >> 0 // 0
'abc' >> 0 // 0

>>>(无符号右移)

该操作符会将第一个操作数向右移动指定的位数,向右被移出的位被丢弃,左侧用 0 填充,它与有符号右移(>>)的区别就是在于负数的处理不同,因为符号位变成了 0,所以结果总是非负的,即便右移 0 个比特,结果也是非负的,对于非负数,有符号右移和无符号右移总是返回相同的结果,例如 9 >>> 29 >> 2 一样返回 2

1
2
3
      9 (base 10): 00000000000000000000000000001001 (base 2)
--------------------------------
9 >>> 2 (base 10): 00000000000000000000000000000010 (base 2) = 2 (base 10)

而如果是负数,结果却有很大不同

1
2
3
      -9 (base 10): 11111111111111111111111111110111 (base 2)
--------------------------------
-9 >>> 2 (base 10): 00111111111111111111111111111101 (base 2) = 1073741821 (base 10)

其实简单来说,例如 length >>> 0,字面上的意思是指『右移 0 位』,但实际上是把前面的空位用 0 填充,这里的作用是保证结果为数字且为整数,再来看下面几个示例加深一下印象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
null >>> 0        // 0

undefined >>> 0 // 0

void (0) >>> 0 // 0

[] >>> 0 // 0

123123 >>> 0 // 123123

45.2 >>> 0 // 45

0 >>> 0 // 0

- 0 >>> 0 // 0

- 1 >>> 0 // 4294967295

- 1212 >>> 0 // 4294966084

function a() { }
a >>> 0 // 0

var a = {}
a >>> 0 // 0

一个综合示例

最后的最后,我们再来看一个综合的使用方式,即使用我们之前的介绍的位运算符来实现一个 rgb 值和 16 进制颜色值之间的转换,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 16 进制颜色值转 RGB
* @param {String} hex 16 进制颜色字符串
* @return {String} RGB 颜色字符串
*/
function hexToRGB(hex) {
var hexx = hex.replace('#', '0x')
var r = hexx >> 16
var g = hexx >> 8 & 0xff
var b = hexx & 0xff
return `rgb(${r}, ${g}, ${b})`
}

/**
* RGB 颜色转 16 进制颜色
* @param {String} rgb RGB 进制颜色字符串
* @return {String} 16 进制颜色字符串
*/
function RGBToHex(rgb) {
var rgbArr = rgb.split(/[^\d]+/)
var color = rgbArr[1] << 16 | rgbArr[2] << 8 | rgbArr[3]
return '#' + color.toString(16)
}

// 使用
hexToRGB('#ccc') // 'rgb(0, 12, 204)'
RGBToHex('rgb(0, 12, 204)') // '#ccc'

参考

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×