前言
在学习js运算符的过程中,大家应该对按位操作符不会感到陌生,但是相信大部分人在编码中用到位操作的机会寥寥,甚至于有些新手同学压根不知道js还有位运算或者某些逻辑运算符。比如一个简单的逻辑运算符 !! ,前阵子组里一个同学来问这是什么意思,解释了大半天。在看各种源码的时候,经常会碰到 !!、~~ 、>> 之类的运算符,刚出道的时候我也是不求甚解,后来看多了自然明了了。
概念
CAUTION:js中的位运算只对整数起作用,因为位操作都有ToInt32这一步,从而舍弃小数部分。
一、先不说位操作运算符,来看两个逻辑运算符
1、!!
要想了解 !! 首先要了解逻辑非运算符 ! ,先看看ECMA-262的定义
产生式 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
2
3
4
5
6
7
8!! 1 // true
!! 0 // false
!! null // false
!! undefined // false
!! NaN //false
!! '' // false
!! 'abc' // true
!! 100 // true
2、~~
同样要想理解 ~~ 首先要知道按位非操作符 ~ 的概念,正如上面MDN截图所示,按位非的意思即对每一个比特位进行非操作求反码再求补码。还是先看ECMA-262的定义
产生式 UnaryExpression : ~ UnaryExpression 按照下面的过程执行 :
1、令 expr 为解释执行 UnaryExpression 的结果 .
2、令 oldValue 为 ToInt32(GetValue(expr)).
3、返回 oldValue 按位取反的结果。结果为 32 位有符号整数。
看一个MDN上的例子:1
2
3
4
5
6
7~9 = -10
// why 是不是感到很奇怪,我又温习了一遍反码补码的知识
// 10进制的数字9转换为二进制原码 0 0000000000000000000000000001001
// 然后对每一位进行按位非 1 1111111111111111111111111110110
// 然后再求反,符号位为1表示为负数,符号位不变,1 0000000000000000000000000001001
// 末尾+1 得到补码 1 0000000000000000000000000001010
// 得到 -10
所以对任一数值 x 进行按位非操作的结果为 -(x + 1)。
明白了按位非 ~ 的意思,下面看 ~~,顾名思义就是在 ~ 的基础上再做一次按位非,等于省略掉定义中的第三步返回 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
二、位操作运算符
1、& (按位与)
& (按位与)应该是最好理解的,如 a&b,意思就是将a和b的每个比特位进行AND运算,即相对应的两个比特位都是1时结果为1,否则为0。将任一数值 x 与 0 执行按位与操作,其结果都为 0。将任一数值 x 与 -1 执行按位与操作,其结果都为 x。1
2
3
4 5 & 6 // 4
// 5的二进制为 101
// 6的二进制为 110
// 得到 100 转成十进制为 4
看一个 & 判断奇偶数的小技巧1
2
3
4
5// 判断奇偶
// 由于数字 1 的二进制是 00000000000000000000000000000001,并且奇数的二进制最低位也是 1
// 所以可以用任意整数和 1 进行&运算判断奇偶
4 & 1 // 0 偶数
5 & 1 // 1 奇数
再看一个复杂点的例子:权限判断
首先假设一个系统有4中权限分比为 一、二、三、四级权限,分别对应的数字为 8(1000)、4(0100)、2(0010)和 1(0001)。
假设有一个用户有二、三级权限,这样数据库中存储的该用户权限标志为 4 + 2 = 6(0110)。
然后判断该用户是否有权限 x ,即可用 6 & x 来判断结果是不是0,如果为0则表示没有该权限。
1 | 6 & 8 // 0 没有一级权限 |
另外也可以用 & 运算实现清除或者保留二进制特定位的作用,就不一一写代码列举了。
2、| (按位或)
| (按位或)即是将a和b的每个比特位进行OR运算,相对应的比特位有一个为1结果就为 1 ,否则为 0 。将任一数值 x 与 0 进行按位或操作,其结果都是 x。将任一数值 x 与 -1 进行按位或操作,其结果都为 -1。1
2
3
4
5
65 | 6 // 7
// 5的二进制为 101
// 6的二进制为 110
// 得到 111 转成十进制为 7
3.1415 | 0 // 3 也可以达到向下取整的效果
上一节的权限判断的例子也可用 | (按位或) 来实现:只需判断 6 | x 结果是不是 6 ,为 6 的话即表示有该权限。1
2
3
46 | 8 // 14 没有一级权限
6 & 1 // 7 没有四级权限
6 & 2 // 6 有三级权限
6 & 4 // 6 有二级权限
3、^ (按位异或)
^ (按位异或) 即是将a和b的每个比特位进行异或(XOR)操作,相对应的比特位有且只有一个为 1 就为1,否则为 0 。再讲明白点就是两个比特位不相同时返回 1 ,一样的时候就为 0。将任一数值 x 与 0 进行异或操作,其结果为 x。将任一数值 x 与 -1 进行异或操作,其结果为 ~x。1
2
3
45 ^ 6 // 3
// 5的二进制为 101
// 6的二进制为 110
// 得到 011 转成十进制为 3
^ 有一个很巧妙的例子:交换两个整数值1
2
3
4
5var a = 5, b = 6;
a ^= b;
b ^= a;
a ^= b;
console.log(a, b); // 6, 5
另外 ^ 也可以实现取绝对值
1 | function abs(a) { |
4、<< (左移)
<< 操作符会将左边的操作数向左移动指定的位数,向左被移出的位将被丢弃,右侧会以0补充。
看一个MDN上的例子:1
2
39 << 2 = 36
// 10进制的数字9转换为二进制原码 00000000000000000000000000001001
// 向左移动两位,丢弃左侧两位,右侧补0得到二进制 00000000000000000000000000100100即10进制 36
所以得到公式 x << y = x * Math.pow(2, y)。看几个例子:1
2
3
4
5
6
7
8
9
10
11
12
13null << 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
5、 >>(有符号右移)
>> 操作符会将左边的操作数向右移动指定的位数,向右被移出的位被丢弃,拷贝最左侧的符号位以填充左侧。由于新的最左侧的位总是和以前相同,符号位没有被改变。所以被称作“符号传播”。
看一个MDN上的例子:1
2
3
4
5
6
79 >> 2 = 2
// 10进制的数字9转换为二进制原码 00000000000000000000000000001001
// 向右移两位,左侧补两位符号位 00 得到 00000000000000000000000000000010 即10进制 2
-9 >> 2 = -3
// 有符号10进制 -9 补码 11111111111111111111111111110111
// 右移两位 11111111111111111111111111111101 即有符号10进制 -3
看看例子:1
2
3
4
5
6
7
8
9
10
11
12null >> 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
Math.floor和 ~~ 、<< 、>> 的关系
上面列举了那么多例子,可能会发现在x为正数的时候 Math.floor(x) 和 ~~ x 、x << 0 、x >> 0 都能够达到向下取整的效果。因为js中所有数字都是以64位浮点数形式储存,当进行位运算的时候都有ToInt32这一步骤舍弃了小数,达到向下取整的效果。
看过好多源码中都有 ~~ x 和 >> 0 等类似向下取整的写法,相对于 Math.floor 写起来比较方便,听说效率也高。为了测试我在jsperf.com上写了个test case mathfloorvsbitwise,发现位运算都比Math.floor快些。