你真的了解JS位运算符吗?

1,752 阅读6分钟

前言:
这是我在掘金的第一篇文章,主题来源于前段时间和网友讨论的一道关于位运算符和逻辑运算符的问题。觉得有必要作为我在掘金的第一次分享。虽然此前一直都有做网络笔记和码字的习惯,但是网络分享却是第一次,所以文笔方面难免生硬,希望谅解。
其次如果内容有错误,请指出,感谢!

位运算的自我检测题:

先来开胃菜帮助热身,如果检测题错误超过一半的话,那么说明你需要这篇文章。

var num1 = 1 & 0;
console.log(num1);
var num2 = 'string' & 1;
console.log(num2);
var num3 = true & 1;
console.log(num3);
var num4 = undefined | false;
console.log(num4);
var num5 = undefined | true;
console.log(num5);
var num6 = 23 & 5;
console.log(num6);
var num7 = 23 | 5;
console.log(num7);
var num8 = 8 << 'string';
console.log(num8);
var num9 = ({}) && 3;
console.log(num9);
var num10 = ({}) & 3;
console.log(num10);
var num11 = ({}) || 3;
console.log(num11);
var num12 = ({}) | 3;
console.log(num12);

至于附带解释的答案,就留到文章后面吧(有的解释需要配合前面的理论讲解更下饭哦~)

什么是位运算:

位运算符用于在最基本的层次上(即按照内存中表示数值的位来操作数值),ECMAScript中的所有数值都以IEEE-754 64位格式来存储,但位操作符并不直接操作64位的值。而是先将64位的值转换为32位的整数,再执行操作,最后再将结果转换为64位。

对于有符号的整数,32位中的后31位用于表示整数的值,第1位表示数值的符号:0表示正数,1表示负数。 正数
正数以纯二进制格式存储,31位中的每1位都表示2的幂(还有1位是符号位)。第一位表示2º,第二位表示2¹,第三位表示2²,以此类推。没有用到的位以0表示(即忽略不计)。

负数
负数同样以二进制码存储,但使用的格式是二进制补码。
计算一个数值的二进制补码(即求一个负数的二进制码),需要经过下列3个步骤:
①.求这个数值绝对值的二进制码(比如要求-18的二进制补码,先求18的二进制码)。
②.求二进制反码,即将0替换为1,将1替换为0。
③.得到的二进制反码+1.

//以求-18的二进制补码为例:
//先求18的二进制码:00000000 00000000 00000000 00010010
//求18的二进制反码:11111111 11111111 11111111 11101101
//得到的二进制反码+1:11111111 11111111 11111111 11101110(这就是-18的二进制反码)

位运算符的底层处理过程:

根据ECMA规范,当使用位运算符时,底层会做以下处理:

非数值应用位操作符:
会先使用Number()函数将该值转换为一个数值(自动完成),然后再应用位操作,得到的结果将是一个数值(number类型)。

数值应用位操作符:
64位的数值将被转换为32位的数值,然后执行位操作,最后再将32位的结果转换为64位数值。(在JavaScript内部,数值都是以64位浮点数的形式储存)。这样表面看起来像是在操作32位数值,就跟在其它语言中以类似的方式执行二进制操作一样。但这个转换过程也导致了一个副作用:就是在对NaN和Infinity值应用位操作时,这两个值都会被当成0来处理。

位运算的特点:

1.位运算直接对二进制位进行计算,位运算直接处理每一个比特位,是非常底层的运算。(优点:速度极快,缺点:很不直观,许多场合不能够使用)。

2.位运算只对整数起作用,如果一个运算数不是整数,会自动将其转为整数后再运行(可以利用位运算取整)。

位运算符都有哪些:

1.按位 与(AND):&

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

2.按位 或(OR):|

| 运算符跟 & 的区别在于如果对应的位中任一个操作数为1,那么结果就是1。(速记:有一则一)

3.按位 异或(XOR):^

^ 如果对应两个操作位 有且仅有1个1时结果为1,其它都是0。(速记:仅1个1为1)

4.按位 非(NOT):~

注1: 在二进制码中,为了区分正负数,采用最高位是符号位的方法来区分,正数的符号位为0、负数的符号位为1.剩下的就是这个数的绝对值部分,可以采用原码、反码、补码3种形式来表示绝对值部分.

注2:手动非~的规则 JavaScript内部采用补码形式表示负数(即需要将这个数减去1), 再取一次反, 表示为十进制后再加上负号, 才能得到这个负数对应的10进制值。

示例1:手动非~:

~ 运算符是对位求反,1变0,0变1,也就是求二进制的反码。
由于11111111 11111111 11111111 11111110中的第1位(符号位)是1,所以这个数是一个负数。

内部补码:11111111 11111111 11111111 11111101
取 反 :00000000 00000000 00000000 00000010 => 即2
表示为十进制后再加上负号:-2

所以 ~1 就是 -2

5.左移(Left shift):<<

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

以1 << 3 为例: 1<<3 =>相当于 1x2³ = 8。

结论:任何数的<<几位数,相当于:

6.有符号右移:>> (又称 “符号传播”)

>>该操作符会将指定操作数的二进制位向右移动指定的位数。向右被移出的位被丢弃,拷贝最左侧的位以填充左侧(这里是和无符号右移的最大区别)。由于新的最左侧的位总是和以前相同,而符号位并没有被改变。所以被称作“符号传播”。

以4 >> 2为例:

7.无符号右移:>>>

>>>该操作符会将第一个操作数向右移动指定的位数。向右被移出的位被丢弃,左侧用0填充(这点是和有符号右移最大的区别)。因为符号位变成了0,所以结果总是非负的。

注:对于非负数,有符号右移和无符号右移总是返回相同的结果。

位运算符和逻辑运算符的区别(&&、&、||、|):

&&、&和||、|在我看来完全是不同的符号(事实也确实如此)。

运算结果不同:
①.作为判断语句判断使用:

if(({}) && 3){
  console.log("true")
}else{
  console.log("false");
}
//输出结果为true

if(({}) & 3){
  console.log("true")
}else{
  console.log("false");
}
//输出结果为false

if(({}) || NaN){
  console.log("true")
}else{
  console.log("false");
}
//输出结果为true
if(({}) | NaN){
  console.log("true")
}else{
  console.log("false");
}
//输出结果为false

②.作为赋值判断语句使用:

var result = ({}) && 3;
console.log(result);//3
result = ({}) & 3;
console.log(result);//0
------------------------------

var result = ({}) || 3;
console.log(result);//{}
result = ({}) | 3;
console.log(result);//3

注:造成运算结果的不同的关键之处在于 逻辑运算符底层对比后,返回的是原值。而位运算符底层对比后,返回的是Number()处理过的值。

底层处理逻辑不同:
||和&&具有短路的功能(即一边满足运算要求,就不会再计算另外一边),而&和|即使一边满足其要求,还会再计算另外一边。

位运算符在JS中的妙用:

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

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

2.使用~、>>、<<、>>>、|来取整:

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

3.使用^来完成值交换:

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

4.使用&, >>, |来完成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'

开文自检的答案加注释

var num1 = 1 & 0;
/**
 * 1的二进制码简写为0001
 * 0的二进制码简写为0000
 * 根据&运算符的特点遇0为0,所以最终结果是0000(答案是0)
 */
console.log(num1);//0

var num2 = 'string' & 1;
/**
 * 'string'是字符串(非数值类型),需要先将其通过Number()转换为数值方可计算
 * Number('string') = NaN
 * 由于NaN、Infinity在数值底层64位转换32位操作中的副作用:这两个值都会被当成0来处理
 * 其次0 & 1 (遇0为0),所以最终结果是 0
 */
console.log(num2);//0

var num3 = true & 1;
/**
 * true是布尔类型,通过Number(true)得1
 * 1 & 1 = 1(&的特性是二者均为1,结果则为1)
 */
console.log(num3);//1

var num4 = undefined | false;
/**
 * undefined是非数值类型,Number(undefined) = NaN(位操作符会将NaN、Infinity当成0)
 * false也是非数值类型,Number(false) = 0
 * NaN | 0 = 0 
 */
console.log(num4);//0

var num5 = undefined | true;
/**
 * undefined是非数值类型,Number(undefined) = NaN(位操作符会将NaN、Infinity当成0)
 * true也是非数值类型,Number(true) = 1
 * NaN | 1 = 1(或运算符的特点为有11) 
 */
console.log(num5);//1

var num6 = 23 & 5;
/**
 * 0001 0111(23的简写)
 * 0000 0101(5的简写)
 * 0000 0101(答案为5)
 */
console.log(num6);//5

var num7 = 23 | 5;
/**
 * 0001 0111(23的简写)
 * 0000 0101(5的简写)
 * 0001 0111(答案为23)
 */
console.log(num7);//23

var num8 = 8 << 'string';
/**
 * 'string'是字符串(非数值类型),Number('string') = NaN
 * 而NaN又会在底层转换过程中转为0,
 * 所以实质是8 << 0 = 8
 */
console.log(num8);//8


var num9 = ({}) && 3;
console.log(num9);//3

var num10 = ({}) & 3;
console.log(num10);//0

var num11 = ({}) || 3;
console.log(num11);//{}

var num12 = ({}) | 3;
console.log(num12);//3