先说下位掩码,这是看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种组合(纯理论,有些组合不一定合理,实际上没有这么多)。
如果用位掩码的方式表示:
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