浅谈JavaScript位操作符

44次阅读

共计 8059 个字符,预计需要花费 21 分钟才能阅读完成。

位操作符的基本概念
因为 ECMAscript 中所有数值都是以 IEEE-75464 格式存储, 所以才会诞生了位操作符的概念.
位操作符作用于最基本的层次上, 因为数值按位存储, 所以位操作符的作用也就是操作数值的位. 不过位操作符并不能操作 64 位的值. 所以位操作符会先将 64 位的值转换成 32 位的值, 然后执行操作, 最后再将结果转换成 64 位的值.
但对于开发人员来说, 这整个过程就像是只存在 32 位的数值一样, 这是因为 64 位存储格式是透明的.
当然这里所说的数值指的是整数. 在对于有符号的整数中,32 位的前 31 位用于表示整数的值, 而第 32 位则表示整数的符号(即 0 表示正数,1 表示负数), 我们把这第 32 个表示符号的位叫做符号位, 符号位也决定了其它位的数值的格式.
正数都是按纯二进制格式存储的, 在前 31 位中的每一个位都表示 2 的幂. 即第一位表示 2^0(2 的零次方), 第二位表示 2^1(2 的 1 次方), 依次类推. 第一位也叫做位 0, 后面依次类推, 第 32 位就叫做位 31, 其它没有用到的位都以 0 填充, 也可以被忽略不计.
比如十进制整数 10 的二进制表示是 0000 0000 0000 0000 0000 0000 0000 1010 或者更简单的 1010。这是 4 个有效位, 这 4 位就决定了实际的值. 在前面说到过可以用 toString()方法指定参数可以表示将一个十进制数转换成二进制数. 所以我在这里写了一个函数, 表示将一个十进制数转换成二进制数, 如下图所示:

既然二进制数 1010 就是十进制数 10, 那么我们还可以将这个二进制数转换成十进制数, 是如何计算的呢? 很简单, 因为二进制数最后一位表示符号, 所以不计, 这里的 101 各代表幂数为 3,2,1, 这也是为什么十进制转换成二进制数要取余数倒排的原因, 然后将位上的数乘以基数 2 的幂数. 也就是说可以写成等式 2^3×1 + 2^2 x 0 + 2^1 x 1 = 10.(2^ 表示 2 的次方).
负数同样以二进制码存储, 只不过与正数有点区别, 区别就是负数的格式是二进制补码. 在求二进制补码的时候, 有以下三个规则:
(1). 先求出这个负数的绝对值的二进制码. 比如十进制数 -17, 就是先求 17 的二进制码.
(2). 然后求二进制码的反码, 就是将 0 变成 1,1 变成 0.
(3)最后将得到的二进制反码加 1.
比如说求十进制数 -10 的二进制码, 我们要先求 10 的二进制码, 也就是 0000 0000 0000 0000 0000 0000 0000 1010, 然后取反码就是 1111 1111 1111 1111 1111 1111 1111 0101, 最后加 1, 但因为二进制数只能是 1 或者 0 表示, 所以 1 + 1 大于 2 的话, 就会向前进位 1. 所以这个反码加 1 最后得到的值应该是 1111 1111 1111 1111 1111 1111 1111 0110. 而这个也是 -10 的二进制表示. 需要注意的是在处理有符号的整数的时候, 是访问不到第 32 位的(也就是位 31).
但在实际情况中,ECMAscript 是会尽力向我们隐藏所有的这些信息. 也就是说在实际转换负数的二进制码时, 它只会将这个负数的绝对值的二进制码前面加上一个负号, 就表示这个负数的二进制码. 如下图所示:

这个转换过程说明 ECMAscript 解析引擎理解了二进制补码并将其以更合乎逻辑的形式展示出来.
在默认情况下,ECMAscript 中的所有整数都是有符号整数. 当然也存在无符号整数, 对于无符号的整数来说, 第 32 位不会再表示符号, 因为无符号整数只能是正整数. 而且无符号整数的值可以更大, 因为第 32 位不再表示符号, 而可以表示成数值. 什么意思呢? 就是说当我们再将十进制数转换成二进制数时, 必须要除到商为 0 时, 才会倒排余数, 而第 32 位恰好就是商为 0 的那个余数. 而正整数值越大, 我们可以省略的有效位数就越多, 此时值也就越大.
在 ECMAscript 中, 当对数值应用位操作符的时候, 虽然后台会发生将 64 位数值转换成 32 位数值, 然后执行完操作之后, 再转换成 64 位的数值这个转换过程. 但正因为这个转换过程导致了一个严重的副效应, 也就是说在对特殊的 NaN 和 Infinity 值应用位操作符时, 这两个值会被当成 0 来处理.
而如果对非数值应用位操作符, 会自动使用 Number()函数将其转换成一个数值来操作, 然后再应用位操作符, 得到的结果也将是一个数值.
总的说来, 位操作符主要包含按位非(NOT), 按位与(AND), 按位或(OR), 按位异或(XOR), 左移, 无符号右移和有符号右移 7 个操作符. 接下来, 咱们就来一一分析这 7 个操作符.
a. 按位非(NOT)

按位非用一个波浪线符号 ”~” 表示, 执行按位非的结果就是取得数值的反码. 它也是 ECMAscript 中少数几个与二进制计算相关的操作符.
比如求 10 的按位非结果, 那么按照求二进制得到 10 的二进制码是 0000 0000 0000 0000 0000 0000 0000 1010, 然后取反码就是 1111 1111 1111 1111 1111 1111 1111 0101. 而要将这个反码转换成十进制数, 还需要以下过程:
此时, 位 31 上的 1 代表符号为负, 因为负数的补码就是反码加 1, 所以得知负数的反码就等于补码减 1, 所以此时求得负数的反码是 1111 1111 1111 1111 1111 1111 1111 0100, 所以负数的原码就是取反, 变成了 0000 0000 0000 0000 0000 0000 0000 1011, 所以此时再将这个二进制数转换成十进制数就是 -(2^3 1+2^2 0+2^1 1 + 2^0 1)=-11. 要理清这个转换过程, 需要知道什么是反码, 什么是原码, 什么又是补码, 因为参与计算的是补码, 而要转换的是求原码. 也就是说, 要想将二进制反码转换成十进制数, 就必须求得二进制反码的原码, 然后对原码直接按照二进制转换成十进制的方式来计算转换. 现在我们来验证一下是否是我们所想的, 如下图所示:

再比如求 -10 的按位非结果, 按照理论分析, 我们从前述可以得知最终 -10 的二进制码为 1111 1111 1111 1111 1111 1111 1111 0110, 取反码就变成了 0000 0000 0000 0000 0000 0000 0000 1001, 而此时的二进制反码的补码, 原码都一样, 所以直接计算就是 2^31+2^20+2^10+2^01=9. 如下图所示:

通过以上示例还应该得到一个结论: 正整数的二进制码的反码与原码补码不一致, 而负整数的二进制码的反码就与原码补码一致. 换句话说, 就是正数的原码与补码一样, 负数的原码与补码不一样.
如果实在是不能理解原码, 补码与反码, 可以直接把这个操作符理解为数值加 1 取反. 如 10 加 1 取反就变成 -11,-10 加 1 取反就变成 9.
而实际上, 对按位非的结果比如~10 与~-10, 我们还可以写成如下图所示的表示:

我们可以用变量来表示, 如下图所示:

虽然不用按位非操作符的以上所表示的代码也能输出同样的结果, 但由于按位非是对底层进行操作, 所以使用按位非操作符的速度会更快.
b. 按位与(AND)

按位与操作符用一个和号字符 (&) 表示, 它有两个操作数, 从本质上讲, 按位与操作就是将数值的每一位二进制码对齐, 然后根据以下规则, 对相同为止上的两个数执行 AND 操作. 规则如下:
第一个数值的位          第二个数值的位           结果
1                             1                     1
1                           0                     0
0                         1                     0
0                     0                     0

简而言之, 就是只在两个数值的位数都对应为 1 的时候, 结果才为 1, 任何一位是 0, 结果都是 0.
如以下示例:

对 10 和 6 进行按位与操作时返回 2, 这是为什么呢? 请看底层原理:
首先 10 转换成二进制数就是 0000 0000 0000 0000 0000 0000 0000 1010, 而 6 转换成二进制数则是 0000 0000 0000 0000 0000 0000 0000 0110. 过程可以如下:
10 = 0000 0000 0000 0000 0000 0000 0000 1010
 6  = 0000 0000 0000 0000 0000 0000 0000 0110
——————————————————————————————————————————————
AND = 0000 0000 0000 0000 0000 0000 0000 0010

然后按位与结果转换成十进制数就是 2^1*1 = 2. 所以最终结果为 2.
再比如求 2 &5 的结果, 现在咱们按照步骤来计算出结果, 然后再验证答案对不对.
首先求得 2 的二进制数为 0000 0000 0000 0000 0000 0000 0000 0010,5 的二进制数为 0000 0000 0000 0000 0000 0000 0000 0101。
2 = 0000 0000 0000 0000 0000 0000 0000 0010
 5  = 0000 0000 0000 0000 0000 0000 0000 0101
——————————————————————————————————————————————
AND = 0000 0000 0000 0000 0000 0000 0000 0000

而这个结果转换成十进制数就是 0。所以得出结果是 0, 现在咱们来验证一下, 如下图所示:

c. 按位或(OR)

   按位或操作符由一个竖线符号 (|) 表示, 同样也有两个操作数. 从本质上讲, 也可以说是将数值的二进制码对齐, 但与按位与操作符有一点点区别, 就是它的规则与按位与操作符不一样, 具体如下:
第一个数值的位          第二个数值的位              结果
1                             1                      1
1                           0                     1
0                         1                      1
0                     0                      0

简而言之, 就是按位或操作符只有其对应的两个位都是 0 的情况下才是 0, 其它有一个位是 1 的情况下都是 1. 如以下示例:

现在, 我们就来分析一下为什么结果是 7, 其实与按位与的底层操作很相似,2 和 5 的二进制数前述示例已求得:
2 = 0000 0000 0000 0000 0000 0000 0000 0010
5  = 0000 0000 0000 0000 0000 0000 0000 0101
——————————————————————————————————————————————
OR = 0000 0000 0000 0000 0000 0000 0000 0111

而将按位或的结果转换成十进制数就是 2^21 + 2^11 + 2^0*1 = 7。所以结果 7 就是这么求来的。
d. 按位异或(XOR)

 按位异或操作符由一个插入符号 (^) 表示, 也有两个操作数, 其本质也与按位与和按位或操作符相同, 但其规则也不一样, 如下:
第一个数值的位         第二个数值的位              结果
1                      1                        0
1                      0                     1
0                      1                        1
0                     0                        0

也就是说, 按位异或操作符只有在其中一个位为 1 时才返回 1, 否则就是 0。如对 2^5 求结果如下图:

现在来分析一下为什么结果是 7, 过程也与求按位与和按位或结果一致.
2 = 0000 0000 0000 0000 0000 0000 0000 0010
5  = 0000 0000 0000 0000 0000 0000 0000 0101
——————————————————————————————————————————————
XOR = 0000 0000 0000 0000 0000 0000 0000 0111

这里因为对应位没有变化, 所以最终结果才会和按位或结果一致。
e. 左移

  左移操作符由两个小于号 (<<) 表示, 也是两个操作数, 第一个操作数就表示要左移的数值, 第二个操作数表示左移的位数. 所以左移操作符的含义就是将数值的所有位向左移动指定的位数.
而在向左移动了指定的左移位数之后, 原数值的右侧会多出指定的位数个空位 (比如指定左移 4 位, 也就多出 4 个空位, 依次类推) 出来, 不过左移操作会自动以 0 来填充这些空位.
如以下示例:

现在来分析一下为什么结果是 40, 首先 5 的二进制数是 0000 0000 0000 0000 0000 0000 0000 0101, 指定的是向左移动 3 位, 所以整体向左移动 3 位, 就变成了 0000 0000 0000 0000 0000 0000 0010  1000, 而这个二进制转换成十进制数就是 2^51 + 2^31 = 40. 所以最终结果就是 40.
注意, 左移操作并不会影响操作数的符号位, 换句话说, 如果将 - 5 左移 3 位, 结果将是 -40, 而不是 40.
f. 右移操作符

 右移操作符又分为无符号右移和有符号右移操作符.
(1). 有符号的右移操作符。

 有符号的右移操作符由两个大于符号表示(>>), 这个操作符的含义就是将数值的位向右移指定的位数, 同时保留符号位的值(正负号标记), 有符号的右移操作符与左移操作符刚好相反, 比如 40 向右移动 3 位就是 5.
同样的, 在移位的过程中, 也会出现空位, 而这时候,ECMAscript 会用符号位的值来填充所有空位, 也就是说每向右移动一位, 移走的位上的数不管是 1, 还是 0 都会消失了, 则会在数值的左侧补充一位, 而这位的值就是符号位的值, 即如果是正数, 补充 0, 负数补充 1.
如以下一个示例:

现在, 咱们就来分析分析为什么最终结果为 0. 首先由前述可以得知 40 的二进制数为 0000 0000 0000 0000 0000 0000 0010  1000, 指定的是向右移动 3 位, 那么整体向右移就变成了 0000 0000 0000 0000 0000 0000 0000 0101, 这个转换成十进制数也就是 5. 所以才会说有符号的右移与左移结果相反.
再来看一个示例:

现在, 咱们就来分析分析为什么最终结果为 0. 首先由前述可以得知 5 的二进制数为 0000 0000 0000 0000 0000 0000 0000 0101, 指定的是向右移动 3 位, 那么整体向右移 3 位, 左侧就要补充符号位的值, 因为是正数(正数符号表示为 0), 所以补充 3 个 0, 就变成了 0000 0000 0000 0000 0000 0000 0000 0000. 所以最终结果为 0。
如果这样不能理解的话, 那么假设向右移动一位, 也就是求 5 >> 1 的结果, 同样在最左侧补充一个符号位的值 0, 右移走了末位的 1. 所以变成了 0000 0000 0000 0000 0000 0000 0000 0010. 这个转换成十进制数就是 2. 现在咱们来操作验证一下, 如下图:

(2). 无符号右移操作符。

   无符号右移操作符由三个大于符号表示(>>>). 这个操作符也是会将所有的 32 位都整体向右移动指定的位数. 对于正数来说, 其实无符号右移操作符和有符号右移操作符的结果一致.
如 5 >>> 1 仍然是 2, 按照同样的过程步骤分析.
对于正数没有什么变化, 但对于负数来说, 变化可就大了, 首先无符号右移操作符是以 0 填充空位, 而不是像有符号右移操作符那样以符号位的值填充. 所以才会正数与有符号右移操作符的结果相同. 但是负数就不一样了, 无符号右移操作符会把负数的二进制码当成正数的二进制码, 而且负数是由其绝对值的二进制补码表示, 因此导致无符号右移之后结果会很大. 换句话说, 就是对负数进行无符号右移操作时只会返回正数.
如求 -5 >>> 3. 我们先自己求一遍, 首先 - 5 的二进制补码为 1111 1111 1111 1111 1111 1111 1111 1010, 而因为无符号右移会把这个补码当成正数的二进制码, 所以转换成十进制数就是 (口算不太现实, 太大了, 还是让计算机来算吧) 如下图所示:

所以就会被当成 4294967290, 然后这个正数的二进制码右移 3 位变成了 0001 1111 1111 1111 1111 1111 1111 1111, 转换成十进制数就是如下图所示:

所以最终结果就是 536870911. 现在, 我们来验证一下, 如下图所示:

前端面试题分析
知道了位操作符之后, 现在咱们来分析一道题, 有这样一道前端面试题, 写一个函数用于判断一个非负整数是否是 2 的非负整数次幂. 而有人曾经这样写, 如下图所示:

那么为什么这样写呢, 我们来分析一下这其中原理, 首先什么是函数, 使用 function 关键字声明的都可以被叫做函数, 而这里定义的函数名也比较语义化, 叫做 isPowerOfTwo, 圆括号中的 n 叫做函数的参数, 顾名思义, 这里的参数就是传入一个非负整数. 而这个函数的作用就是要判断传入的参数 (即非负整数) 是否是 2 的非负整数次幂.
return 也是一个关键字, 表示返回一个值, 用在函数当中, 而要记住的是, 如果在函数当中写入了 return 关键字, 在这个关键字表示的语句结束后面再写其它语句是没有效果的, 如下图所示:

如上图所示,alert()方法表示弹出一个原生的弹出框, 但实际上在调用这个定义的判断函数之后, 是不会执行弹出框的, 这就是 return 关键字在这里起到的作用.
现在再来分析一下里面的结构, 叹号 (!) 也就是逻辑非的意思, 这个操作符会把一个操作数转换成布尔值, 然后取反.
再来看圆括号里面的和字符号 &, 在学了位操作符之后, 我们就应该知道这个符号就是按位与的意思, 而按位与是操作二进制数的位的, 对应规则也应该知道, 就是当两个操作数 (在这里指 n 和 n -1) 的对应位都是 1 时, 最终返回的对应位结果才是 1, 否则就是 0. 按位与的作用就是将位对齐. 所以, 在返回这个结果之前, 我们还需要知道如何转换成二进制数.
我们应该知道对象的 toString()方法, 可以为其指定一个参数为基数 2, 就可以将一个操作数转换成二进制数返回, 当然这里也是返回一个字符串. 而为了方便, 我将这个方法封装在一个函数中, 如下图所示:

现在我们再来看看一个非负整数如果是 2 的幂, 会有什么特点, 我们可以调用以上的定义函数将一个非负整数转换成二进制数, 而一个非负整数如果是 2 的幂, 我们应该知道 2 的幂有 2^0 = 1,2^1 = 2,2^2 = 4…… 依次类推, 我们从而得知 1,2,4,8,16…. 等就是 2 的幂, 而我们将这些值转换成二进制数, 就可以知道有什么样的关系了, 比如 1 转换成二进制就是 1,2 转换成二进制是 10,4 转换成二进制是 100…… 依此类推, 不信咱们可以用上面定义好的函数来验证, 如下图:

现在我们就应该知道规律了, 如果一个非负整数是 2 的非负整数次幂的话, 那么这个数一定是上一个 2 的非负整数次幂的二进制数左移了一位. 而通过之前知道的左移操作符, 我们知道, 左移就是将位往左移动一位, 然后在移动后的空位中以 0 填充.
现在, 我们再来看看 n -1, 假设是 2 的非负整数次幂的非负整数, 减 1, 然后再将其转换成二进制数, 比如 1 是 2 的非负整数次幂,1-1 =0. 转换成二进制就是 0(这里是简写), 再比如 2 -1= 1 的二进制就是 1,4-1= 3 的二进制就是 11,7 就是 111. 不信我们可以通过以上定义的函数来验证, 如下图所示:

通过使用按位与操作符取得非负整数与非负整数减 1 的结果, 不言而喻, 始终都会返回 0, 为什么呢? 因为对应位的关系, 我们取其中一个为例子, 如下:
0 = 0000 0000 0000 0000 0000 0000 0000 0000
1 = 0000 0000 0000 0000 0000 0000 0000 0001
——————————————————————————————————————————————
AND = 0000 0000 0000 0000 0000 0000 0000 0000

所以最终结果就是二进制数 0000 0000 0000 0000 0000 0000 0000 0000, 转换成十进制数就是 0.
这样, 我们就应该知道了, 如果这个非负整数是 2 的非负整数次幂的话, 那么它与它减 1 两个操作数取按位与结果就应该是 0.
而我们知道逻辑非操作符对数值 0 会返回 true 的布尔值, 所以当如果传入的参数是非负整数, 并且还是 2 的非负整数次幂的话, 那么这个函数最终就会返回 true. 我们可以直接调用这个函数, 如下图所示:

理解和掌握 JavaScript 位操作符,有助于我们研究底层原理。

正文完
 0