位运算博大精深,本文总结下基本的位运算的概念。
1、整数的二进制码
位操作符用于在最基本的层次上,即按内存中表示数值的位来操作数值。ECMAScript中的所有数值都以IEEE-754 64位格式存储,但位操作符并不直接操作64位的值。而是先将64位的值转换成32位的整数,然后执行操作,最后再将结果转换回64位。对于开发人员来说,由于64位存储格式是透明的,因此整个过程就像是只存在32位的整数一样。
对于有符号的整数,32位中的前31位用于表示整数的值。第32位用于表示数值的符号:0表示正数,1表示负数。这个表示符号的位叫做符号位。例如,数值18的二进制表示是00000000000000000000000000010010
,或者更简洁的10010
。
负数同样以二进制码存储,但使用的是二进制补码(其实正数也是用补码表示)。计算一个数值的二进制补码,需要经过下列三个步骤:
- 求这个数值绝对值的二进制码
- 求二进制反码,即将0替换为1,将1替换为0
- 得到的二进制反码加1
比如求-18的二进制码,首先求得18的二进制码:
0000 0000 0000 0000 0000 0001 0010
然后求其二进制反码:
1111 1111 1111 1111 1111 1110 1101
最后,二进制反码+1
1111 1111 1111 1111 1111 1110 1110
这样就求得了-18的二进制表示。
在ECMAScript中,当对数值应用位操作符时,后台会发生如下转换过程:64位的数值被转换成32位数值,然后执行位操作,最后再将32位的结果转换回64位数值。这样,表面上看起来就好像是在操作32位数值。但这个转换过程也导致了一个严重的负效应,即在对特殊的NaN和Infinity值应用位操作时,这两个值都会被当成0来处理。如果对非数值进行位操作,会先使用Number()函数将该数值转换成一个数值(自动完成),然后再应用位操作,得到的结果是一个数值。
2、~ & | ^
接下来介绍4个位操作符。
按位非操作符由一个波浪线(~)表示,执行按位非的结果就是返回数值的反码。
var num = 25;
console.log(~num); // -26
对25执行按位非操作,结果得到了-26,这也验证了之前说的结论,“一个负数的二进制码是该数绝对值的反码+1”
按位与操作符由一个和号字符(&)表示,它有两个操作符数,按位与操作只在两个数值的对应位都是1时才返回1,否则0。
按位或操作符由一个竖线符号(|)表示,同样也有两个操作符,在有一位是1的情况下返回1,否则0.
按位异或操作符由一个插入符号(^)表示,在两个数值对应位上只有一个1时返回1,否则0。
3、<< >> >>>
左移符号由两个小于号(<<)表示,这个操作符会将数值的所有位向左移动指定的位数。在左移后,原数值右侧空出的位由0填补。
左移一位其实就相当于将原数值乘以2,左移不会影响操作数的符号位。
右移操作符由两个大于号(>>)表示,这个操作符会将数值向右移动,但保留符号位。在移位过程中,空缺位出现在原数值的左侧,符号位的右侧,用符号位的值来填充空位。右移一位相当于原数除2后向下取整。
无符号右移操作符由三个大于号(>>>)表示,这个操作符会将数值的所有32位都向右移动。对正数来说,无符号右移的结果和有符号相同,但是对负数来说就不一样了,无符号右移会把负数的符号位也进行移动,左边空出位置用0填充。
var num = -64;
console.log(num >> 5); // -2
console.log(num >>> 5); // 134217726
对-64进行无符号右移操作,将其用二进制码表示:
1111 1111 1111 1111 1111 1111 1100 0000
右移5位后:
0000 0111 1111 1111 1111 1111 1111 1110
即十进制的134217726。
notice:并没有无符号左移!
4、关于位运算的坑
了解的基本的位运算操作后,这里我要谈谈我经常碰到的一个坑。
int32的取值范围是-2^31 ~ 2^31-1,于是我经常会去求1<<31的值,但是是溢出的...
console.log(1 << 31); // -2147483648
原因很简单,1的左边只有30位可以移动,实际上把1移到了符号位上,得到了:
1000 0000 0000 0000 0000 0000 0000 0000
所以要表示int32的区间范围,可以这样:
console.log(1 << 31); // -2147483648
console.log(1 << -1); // -2147483648
console.log(-0x80000000); // -2147483648
console.log(~(1 << 31)); // 2147483647
console.log(-(1 << -1) - 1); // 2147483647
console.log(0x7fffffff); // 2147483647
console.log(Math.pow(2, 31) - 1); // 2147483647
console.log((1 << 30) * 2 - 1); // 2147483647