位运算移位运算符

位运算符将它的操作数视为 32 位元的二进制串(0 和 1 组成)而非十进制、八进制或十六进制数。例如:十进制数字 9 用二进制表示为 1001,位运算符就是在这个二进制表示上执行运算,但是返回结果是标准的 JavaScript 数值。

  • 所有 JavaScript 数字存储为根为 10 的 64(8 比特)浮点数。JavaScript 不是类型语言。与许多其他编程语言不同,JavaScript 不定义不同类型的数字,比如整数、短、长、浮点等等。
  • 整数精度(不使用小数点或指数计数法)最多为 15 位。小数精度的最大位数是 17,但是浮点运算并不总是 100% 准确。
  • 位运算直接对二进制位进行计算,位运算直接处理每一个比特位,是非常底层的运算,好处是速度极快,缺点是很不直观,许多场合不能够使用。
  • 位运算只对整数起作用,如果一个运算数不是整数,会自动转为整数后再运行。
  • 在 JavaScript 内部,数值都是以 64 位浮点数的形式储存,但是做位运算的时候,是以 32 位带符号的整数进行运算的,并且返回值也是一个 32 位带符号的整数。

位运算符

JavaScript 中共有 7 个位运算符。

按位与

按位与(AND)& 以特定的方式组合操作二进制数中对应的位,如果对应的位都为 1,那么结果就是 1, 如果任意一个位是 0 则结果就是 0。

// 1的二进制表示为: 00000000 00000000 00000000 00000001
// 3的二进制表示为: 00000000 00000000 00000000 00000011
// -----------------------------
// 1的二进制表示为: 00000000 00000000 00000000 00000001
console.log(1 & 3);
// 1

按位或

按位或(OR)| 运算符与 & 的区别在于如果对应的位中任一个操作数为 1 那么结果就是 1。

// 1的二进制表示为: 00000000 00000000 00000000 00000001
// 3的二进制表示为: 00000000 00000000 00000000 00000011
// -----------------------------
// 3的二进制表示为: 00000000 00000000 00000000 00000011
console.log(1 | 3);
// 3

按位异或

按位异或(XOR)^ 如果对应两个操作位有且仅有一个 1 时结果为 1,其他都是 0。

// 1的二进制表示为: 00000000 00000000 00000000 00000001
// 3的二进制表示为: 00000000 00000000 00000000 00000011
// -----------------------------
// 2的二进制表示为: 00000000 00000000 00000000 00000010
console.log(1 ^ 3);
// 2

按位非

按位非(NOT)~ 运算符是对位求反,1 变 0, 0 变 1,也就是求二进制的反码。

// 1的二进制表示为: 00000000 00000000 00000000 00000001
// 3的二进制表示为: 00000000 00000000 00000000 00000011
// -----------------------------
// 1反码二进制表示: 11111111 11111111 11111111 11111110
// 由于第一位(符号位)是1,所以这个数是一个负数。JavaScript 内部采用补码形式表示负数,即需要将这个数减去 1,再取一次反,然后加上负号,才能得到这个负数对应的 10 进制值。
// -----------------------------
// 1的反码减1: 11111111 11111111 11111111 11111101
// 反码取反: 00000000 00000000 00000000 00000010
// 表示为10进制加负号:-2
console.log(~1);
// -2

简单记忆: 一个数与自身的取反值相加等于 -1。

左移

左移(Left shift)<< 运算符使指定值的二进制数所有位都左移指定次数,其移动规则:丢弃高位,低位补 0,即按二进制形式把所有的数字向左移动对应的位数,高位移出(舍弃),低位的空位补零。

// 1的二进制表示为: 00000000 00000000 00000000 00000001
// -----------------------------
// 2的二进制表示为: 00000000 00000000 00000000 00000010
console.log(1 << 1);
// 2

有符号右移

有符号右移 >> 会将指定操作数的二进制位向右移动指定的位数。向右被移出的位被丢弃,拷贝最左侧的位以填充左侧。由于新的最左侧的位总是和以前相同,符号位没有被改变。所以被称作 符号传播

// 1的二进制表示为: 00000000 00000000 00000000 00000001
// -----------------------------
// 0的二进制表示为: 00000000 00000000 00000000 00000000
console.log(1 >> 1);
// 0

无符号右移

无符号右移 >>> 会将第一个操作数向右移动指定的位数。向右被移出的位被丢弃,左侧用 0 填充。因为符号位变成了 0,所以结果总是非负的。(译注:即便右移 0 个比特,结果也是非负的。) 对于非负数,有符号右移和无符号右移总是返回相同的结果。例如,9 >>> 2 得到 2 和 9 >> 2 相同。

总结

运算符用法描述
按位与a & ba b的位表示中,每一个对应的位都为 1 则返回 1, 否则返回 0
按位或a | ba b的位表示中,每一个对应的位,只要有一个为 1 则返回 1, 否则返回 0
按位异或a ^ ba b的位表示中,每一个对应的位,两个不相同则返回 1,相同则返回 0
按位非~ a反转被操作数的位
左移a << ba 的二进制串向左移动 b 位,右边移入 0
算术右移a >> ba 的二进制表示向右移动 b 位,丢弃被移出的所有位
无符号右移a >>> ba 的二进制表示向右移动 b 位,丢弃被移出的所有位,并把左边空出的位都填充为 0

最佳实践

取整

使用 ~>><<>>>| 来取整。

console.log(~~6.83); // 6
console.log(6.83 >> 0); // 6
console.log(6.83 << 0); // 6
console.log(6.83 | 0); // 6
// >>>不可对负数取整
console.log(6.83 >>> 0); // 6

值交换

使用按位异或 ^ 来完成值交换

var a = 5;
var b = 8;
a ^= b;
b ^= a;
a ^= b;
console.log(a); // 8
console.log(b); // 5

异或还经常被用于加密。

十进制转二进制

var number = 3;
var result = number.toString(2);
var result2 = (14).toString(2);
// "1110"

颜色值转换

使用 &>>| 来完成 RGB 值和 16 进制颜色值之间的转换

/**
* 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('#ffffff'); // 'rgb(255,255,255)'
RGBToHex('rgb(255,255,255)'); // '#ffffff'

判断正负

function isPos(n) {
return n === n >>> 0 ? true : false;
}
isPos(-1); // false
isPos(1); // true

判断符号是否相同

通常, 比较两个数是否符号相同, 我们使用 x * y > 0 来判断即可. 但如果利用按位异或 ^, 运算速度将更快。

console.log(-17 ^ (9 > 0));
// false

判断奇偶

使用 & 运算符判断一个数的奇偶

如果把 n 以二进制的形式展示的话,其实我们只需要判断最后一个二进制位是 1 还是 0 就行了。

// 偶数 & 1 = 0
// 奇数 & 1 = 1
console.log(2 & 1); // 0
console.log(3 & 1); // 1

判断索引是否存在

这是一个很常用的技巧,如判断一个数是否在数组里面:

// 如果url含有 ? 号,则后面拼上&符号,否则加上?号
url += ~url.indexOf('?') ? '&' : '?';

因为:~-1 === 0

-1 在内存的表示的二进制符号全为 1,按位非之后就变成了 0. 进一步说明——1 在内存的表示为:0000...0001,第一位 0 表示符号位为正,如果是 -1 的话符号位为负用 1 表示 1000...0001,这个是 -1 的原码,然后符号位不动,其余位取反变成 1111...1110,这个就是 -1 的反码表示,反码再加 1 就变成了 1111...1111,这个就是 -1 的补码,负数在内存里面(机器数)使用补码表示,正数是用原码。所以全部都是 1 的机器数按位非之后就变成了全为 0。剩下的其它所有数按位非都不为 0,所以利用这个特性可以用来做 indexOf 的判断,这样代码看起来更简洁一点。

标志位判断

现在有个后台管理系统,操作权限分为一级、二级、三级管理员,其中一级管理员拥有最高的权限,二、三级较低,有些操作只允许一、二级管理员操作,有些操作只允许一、三级管理员操作。现在已经登陆的某权限的用户要进行某个操作,要用怎样的数据结构能很方便地判断他能不能进行这个操作呢?

我们用位来表示管理权限,一级用第 3 位,二级用第 2 位,三级用第 1 位,即一级的权限表示为 0b100 = 4,二级权限表示为 0b010 = 2,三级权限表示为 0b001 = 1。如果 A 操作只能由一级和二级操作,那么这个权限值表示为 6 = 0b110,它和一级权限与一下:6 & 4 = 0b110 & 0b100 = 4,得到的值不为 0,所以认为有权限,同理和二级权限与一下 6 & 2 = 2 也不为 0,而与三级权限与一下 6 & 1 = 0,所以三级没有权限。这里标志位的 1 表示打开,0 表示关闭。

这样的好处在于,我们可以用一个数字,而不是一个数组来表示某个操作的权限集,同时在进行权限判断的时候也很方便。


参考资料: