JavaScript中的位运算

先说下位掩码,这是看JavaScript位运算的初衷。

位掩码的概念

什么是位掩码,先看个有趣的智力题,据说也是很多公司的面试题目。

小白鼠试毒问题

有1000瓶药水,其中只有一瓶有毒,小白鼠喝了会死。问,最多需要多少只小白鼠可以试出来哪瓶有毒?

答案:

将1000瓶药水用二进制编码,第一瓶表示为01, 第二瓶表示为10, 第三瓶表示为11,……,第1000瓶表示为1111101000。将这1000个二进制数进行|(按位或)运算得到一个数值(当然结果也是二进制),我们暂且称这个数值为小B。这个按位或|运算步骤对应我们的实际操作是,将这些二进制数中当前位是1的药水进行混合,最终得到10瓶(因为小B的长度就是10嘛)新的混合药水。位。那么我们用10只小白鼠分别去喝这10瓶药水。那么24小时候,10只小白鼠有生有死。生记为0, 死记为1,按之前的顺排列好后又得到一个二进制数, 我们称之为小C。最后,我们用小B跟小C进行&(按位与)运算,得到的二进制结果,就是有毒药水瓶的二进制编号。

下面是一个简化版的题目,只有3瓶药水,用于验证上述步骤的正确性。

// 三瓶药水分别记为 Y1, Y2, Y3
// 0b开头是二进制数的表示语法
var Y1 = 0b01,
    Y2 = 0b10,
    Y3 = 0b11;
// 实际上,为了方便表示,Y1, Y2, Y3可以分别用十进制数1,2,3表示


var B = Y1 | Y2 | Y3; // 按位或运算;
/**
Y1:  0 1
Y2:  1 0
Y3:  1 1
     ----
      1 1
所以B = 0b11;
*/
console.log(B.toString(2).length);
// 上面可以看到B的长度是2,那么我们需要两只小白鼠
// 两只小白鼠分别记做 M1, M2
// 根据B的运算过程,我们知道小白鼠M1喝Y2,Y3, 小白鼠M2喝Y1, Y3

// 接下来我们要验证结果,假设第一瓶Y1有毒, 那么小白鼠M1活,M2死,得到结果是 0b01; 
var C = 0b01;
console.log( (B&C) === Y1 ); // true , 没有问题

// 如果第二瓶有毒,那么小白鼠M1死,小白鼠M2活, 等到结果0b10;
C = 0b10;
console.log( (B&C) === Y2 ); // true 

这个过程感觉很神奇啊~~~~

位掩码的实际场景

最开始看到这个概念,是JavaScript中的compareDocumentPosition()方法。这个方法用于获取连个DOM节点之间的关系(祖先、后代、之前、之后等)。返回结果是一个表示二者关系的位掩码。
最开始不理解为什么会用 位掩码,然后去查一些资料。那么上结论:因为两个DOM节点之间可能存在多种关系,不同关系的组合如果用枚举可能会导致数据量比较大,且如果增加一种新的关系,那么变动也比较大,不易增加。 用位掩码就能解决上面这个问题。

拿上面的这个compareDocumentPosition()方法举例:

两个DOM节点A、B之间的可能的关系:

  • 无关:A和B节点之间没有任何关系
  • 祖先:A 是B的祖先节点
  • 后代:A是B的后代节点
  • 之前:A在B节点之前
  • 之后:A在B节点之后

如果用枚举法的话,他们理论上一共有2^5 = 32种组合(纯理论,有些组合不一定合理,实际上没有这么多)。

扫描二维码关注公众号,回复: 2205824 查看本文章

如果用位掩码的方式表示:

var R1 = 1,  // 无关
    R2 = 2,  // 之前
    R3 = 4,  // 之后
    R4 = 8,  // 祖先
    R5 = 16; // 后代

// compareDocumentPosition()方法返回的一定是其中某种关系的组合
// 比如R3和R5的组合R =  R3 | R5 ; // 20
// 只要判断R中有没有R3,就可以判断两节点间是否成立‘之后’关系
!!(20 & R3); // true
!!(20 & R1); // false
!!(20 & R2); // false
!!(20 & R5); // true
!!(20 & R4); // false

下面仔细讲讲JS中的位运算,首先明确一点:在JavaScript中,用一个32位的二进制数表示数值,其中第32位表示数值的符号(正数或负数)。
比如18的二进制表示方式是:
0000 0000 0000 0000 0000 0000 0001 0010

而负数的表示方式,
1. 先取数值绝对值的二进制码n
2. 取得n的反码b
3. b+1 就是负数的二进制码

按位非(NOT)

按位非的操作符是一个波浪线~,执行按位非的操作,结果就是取 数值的反码。

var n = 3;
~n; // -4
// 3的二进制表示法 0000 0000 0000 0000 0000 0000 0000 0011
// 那么3 的二进制码 的反码是
// 1111 1111 1111 1111 1111 1111 1111 1100

按位与(AND)

按位与操作符由一个和号字符(&)表示,有两个操作数。它的运算规则是将两个操作数(二进制形式)的每一位对齐,然后跟据一下几条规则进行计算。
1. 只有同是1的时候,结结果才是1;
2. 其它任何情况都是0;

比如 5 & 4的计算过程:

// 下面的二进制表示法,我们只取最后四位,因为前面的都是0,
// 5 的 二进制表示: 0101
// 4 的 二进制表示: 0100

// 0 1 0 1
// 0 1 0 0
// -------
// 0 1 0 0
console.log(5&4); // 4

按位或(OR)

按位或操作符由一个竖线符号(|)表示,同样也有两个操作数。它的运算规则是将两个操作数(二进制形式)的每一位对齐,然后跟据一下几条规则进行计算。
1. 只有同是0的时候,结结果才是0;
2. 其它任何情况都是1;

跟按位与的运算相反。

// 下面的二进制表示法,我们只取最后四位,因为前面的都是0,
// 5 的 二进制表示: 0101
// 4 的 二进制表示: 0100

// 0 1 0 1
// 0 1 0 0
// -------
// 0 1 0 1
console.log(5&4); // 5

按位异或(XOR)

按位异或操作符由一个插入符号(^)表示,也有两个操作数。它的运算规则是将两个操作数(二进制形式)的每一位对齐,然后跟据一下几条规则进行计算。
1. 两位同是0或1的时候,结结果才是0;
2. 其它任何情况都是1;

这个运算跟按位或有略微差异,注意区分两位都是1的情况。

// 下面的二进制表示法,我们只取最后四位,因为前面的都是0,
// 5 的 二进制表示: 0101
// 4 的 二进制表示: 0100

// 0 1 0 1
// 0 1 0 0
// -------
// 0 0 0 1
console.log(5&4); // 1

左移

左移操作符由两个小于号(<<)表示,这个操作符会将数值的所有位向左移动指定的位数。右边空出来的位置,补0;

// 下面的二进制表示法,我们只取最后四位,因为前面的都是0,
// 5 的 二进制表示: 0101
// 将5向左移动4位得到二进制结果是: 0101 0000 
// 将左移后二进制数0101 0000 转换成十进制是 2^6 + 2^4 = 64 + 16 = 80
console.log(5<<4); // 80

有符号的右移

有符号的右移操作符由两个大于号(>>)表示,这个操作符会将数值向右移动,但保留符号位(即 正负号标记)。有符号的右移操作与左移操作恰好相反。

// 下面的二进制表示法,我们只取最后四位,因为前面的都是0,
// 80 的 二进制表示: 0101 0000
// 将80向右移动4位得到二进制结果是: 0101
console.log(80>>4); // 5

无符号的右移

无符号右移操作符由 3 个大于号(>>>)表示,这个操作符会将数值的所有 32 位都向右移动。对正 数来说,无符号右移的结果与有符号右移相同。

但是对负数来说,情况就不一样了。首先,无符号右移是以 0 来填充空位,而不是像有符号右移那样以符号位的值来填充空位。所以,对正数的无符号右移与有符号右移结果相同,但对负数的结果就不一样了。其次,无符号右移操作符会把负数的二进制码当成正数的二进制码。而且,由于负数以其绝对 值的二进制补码形式表示,因此就会导致无符号右移后的结果非常之大。

下面看个例子:

var m = -1;
console.log(m>>>3); // 536870911
console.log(m>>3); // -3
// 先取一个比较大的正整数,它是2的31次方,这样满足二进制码表示时,只有第31位是1,后面的都是0;
var n = Math.pow(2, 31);
// 那么我们看下 -n>>>31 的结果
console.log( (-n>>>31)); // 1
console.log( (n>>31)); // 1

猜你喜欢

转载自blog.csdn.net/u013137242/article/details/80889924