1

MDN 描述,按位操作符(Bitwise operators) 将其操作数(operands)当作32位的比特序列,即按位操作符的数会被转化成32位的二进制数进行操作。
在介绍 JavaScript 的按位操作符之前要先了解计算机二进制数中的几个基本概念:原码、反码和补码。

原码、反码和补码

如果我们在内存中分配4位去存放二进制数,正数十进制和二进制的关系如下图所示:

十进制二进制(4位)
00000
10001
20010
30011

为了区别负数,引入了原码概念,用左边的第一位数表示符号。如果是负数,那就要将左边第一位改成1,负数十进制对应的二进制4位原码如下图所示:

十进制二进制(4位)
-01000
-11001
-21010
-31011

但是这样衍生出一个问题,正数和负数原码的和不为0。

// -1 + 1
0001 + 1001 = 1010 // -2

为了解决这个问题,出现了 反码 概念。用原码求反码的方法是:正数不变,负数的符号为不变,其他位取反。负数十进制对应的二进制4位原码和反码对应表格如下图所示:

十进制二进制原码(4位)二进制反码(4位)
-010001111
-110011110
-210101101
-310111100

此时再来算和

// -1 + 1
1001(原码) + 0001(原码) = 1110(反码) + 0001(反码) = 1111(反码) // 1111 就是 -0 的反码,所以结果是 -0

此时正数和负数的和是-0,在二进制原码中,-0 和 +0 的表现是有区别的,+0 是 0000,而 -0 是 1000,而 -1 与 1 的和显然不应该是 -0,这时候就又可以提出 补码 的概念:正数的补码是它本身,负数的补码是它的反码 + 1。负数十进制对应的二进制4位原码、反码和补码对应表格如下图所示:

十进制二进制原码(4位)二进制反码(4位)二进制补码(4位)
-0100011110000
-1100111101111
-2101011011110
-3101111001101

可以看出 -0 的补码是 0000,而 +0 的原码是 0000,补码也是 0000,所以它们的补码一致。

// -1 + 1
1001(原码) + 0001(原码) = 1111(补码) + 0001(补码) = 0000(补码) // 0000 是 -0 和 0 公用的补码,所以 -1 + 1 的和是 0

所以二进制数参与计算的最优解是用补码计算。
总结一下原码、反码和补码的关系

非负数的原码 = 反码 = 补码
负数的反码 = 原码除符号位取反
负数的补码 = 反码 + 1

JavaScript 中的按位操作符

JavaScript 中的常用按位操作符及描述如下表所示(下表格完全照抄 MDN),所有的按位操作符的操作数都会被转成补码(two's complement)形式的有符号32位整数。

运算符用法描述
按位与( AND)a & b对于每一个比特位,只有两个操作数相应的比特位都是1时,结果才为1,否则为0。
按位或(OR)ab对于每一个比特位,当两个操作数相应的比特位至少有一个1时,结果为1,否则为0。
按位异或(XOR)a ^ b对于每一个比特位,当两个操作数相应的比特位有且只有一个1时,结果为1,否则为0。
按位非(NOT)~ a反转操作数的比特位,即0变成1,1变成0。
左移(Left shift)a << b将 a 的二进制形式向左移 b (< 32) 比特位,右边用0填充。
有符号右移a >> b将 a 的二进制表示向右移 b (< 32) 位,丢弃被移出的位。
无符号右移a >>> b将 a 的二进制表示向右移 b (< 32) 位,丢弃被移出的位,并使用 0 在左侧填充。

其中重点介绍一下下面两个

~ (按位非)

对每一个比特位执行非(NOT)操作。NOT a 结果为 a 的反转(即反码)。

5 (base 10) = 00000000000000000000000000000101 (base 2)
~5 (base 10) = 11111111111111111111111111111010 (base 2) = -6 (base 10) // -6 的补码

对任一数值 x 进行按位非操作的结果为 -(x + 1)。

>> (有符号右移)

无符号左移和无符号右移填充的数据都是 0 ,而有符号右移填充的数据是最左侧数据。下面举两个例子

正数的有符号右移

正数的有符号右移在最左边填充 0

9 >> 2
9 (base 10): 0000000000000000000000000001001 (base 2)
9 >> 2 (base 10): 00000000000000000000000000000010 (base 2) = 2 (base 10)

负数的有符号右移在最左边填充 1

-1 >> 2
-1 (base 10): 11111111111111111111111111111111 (base 2)
-1 >> 2 (base 10): 11111111111111111111111111111111 (base 2) = -1 (base 10) // -1 的补码

按位操作符在 JavaScript 中的一些用法

说了这么多,还是要看看按位操作符在 Js 中具体有哪些应用场景。

用按位与 & 判断奇偶

用 按位与 & 可以给数判断奇偶

// 偶数 & 1 = 0
// 基数 & 1 = 1

用按位异或 ^ 数据交换

如果想实现两个数互换,但是不想新建变量,可以用异或 ^

let a = 5
let b = 6
a ^= b // 3
b ^= a // 5
a ^= b // 6

用标志位判断类型(标志位与掩码)

例子参考链接 枚举值 VNodeFlags,下文的图片和分类均出自该参考链接。
Vue 组件的产出是 VNode,渲染器渲染的目标也是 VNode。在这里把 VNode 分成五类,分别是:html/svg 元素、组件、纯文本、Fragment 以及 Portal:
vnode 的种类

const VNodeFlags = {
  // html 标签
  ELEMENT_HTML: 1,
  // SVG 标签
  ELEMENT_SVG: 1 << 1,

  // 普通有状态组件
  COMPONENT_STATEFUL_NORMAL: 1 << 2,
  // 需要被keepAlive的有状态组件
  COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE: 1 << 3,
  // 已经被keepAlive的有状态组件
  COMPONENT_STATEFUL_KEPT_ALIVE: 1 << 4,
  // 函数式组件
  COMPONENT_FUNCTIONAL: 1 << 5,

  // 纯文本
  TEXT: 1 << 6,
  // Fragment
  FRAGMENT: 1 << 7,
  // Portal
  PORTAL: 1 << 8
}

根据上面的分类图,可以派生出额外的三个标识。这里用了 按位或 | 操作符,

// html 和 svg 都是标签元素,可以用 ELEMENT 表示
VNodeFlags.ELEMENT = VNodeFlags.ELEMENT_HTML | VNodeFlags.ELEMENT_SVG
// 普通有状态组件、需要被keepAlive的有状态组件、已经被keepAlice的有状态组件 都是“有状态组件”,统一用 COMPONENT_STATEFUL 表示
VNodeFlags.COMPONENT_STATEFUL =
  VNodeFlags.COMPONENT_STATEFUL_NORMAL |
  VNodeFlags.COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE |
  VNodeFlags.COMPONENT_STATEFUL_KEPT_ALIVE
// 有状态组件 和  函数式组件都是“组件”,用 COMPONENT 表示
VNodeFlags.COMPONENT = VNodeFlags.COMPONENT_STATEFUL | VNodeFlags.COMPONENT_FUNCTIONAL

这时我们新建一个变量,并用 按位与 & 来判断它的类型

// STATEFUL_NORMAL_COMPONENT 是一个普通有状态组件
const STATEFUL_NORMAL_COMPONENT = VNodeFlags. COMPONENT_STATEFUL_NORMAL

STATEFUL_NORMAL_COMPONENT &  VNodeFlags.COMPONENT_STATEFUL_NORMAL // 真
STATEFUL_NORMAL_COMPONENT & VNodeFlags.COMPONENT_STATEFUL // 真
STATEFUL_NORMAL_COMPONENT & VNodeFlags.COMPONENT // 真
STATEFUL_NORMAL_COMPONENT & VNodeFlags.ELEMENT // 假

当然这里我们也可以用数组的方法来判断,但是相比之下还是使用按位操作符更加简介和直观。
按位操作符的一些其他应用可以参考 位运算符在JS中的妙用,我这里只是举了几个例子。

参考


MrBigShot
4.8k 声望3.1k 粉丝

菜鸡一个