位掩码的使用

4,445 阅读9分钟
  • [时间]: 2019/10/15
  • [keyword]: 位掩码, BitMask

本文部分内容引用自互联网和 MDN, 前半部分会讲位运算, 后部分讲位掩码.

位运算

MDN 的文档写的太好了就直接拷贝过来了, 有兴趣的小伙伴位运算部分可以直接去看 MDN

位运算即对数字的二进制形式进行运算, JavaScript 内置的有:

运算符 用法 描述
按位与( AND) a & b 对于每一个比特位,只有两个操作数相应的比特位都是1时,结果才为1,否则为0。
按位或(OR) a | b 对于每一个比特位,当两个操作数相应的比特位至少有一个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 在左侧填充。

在一些其他的语言中, 例如 Haskell, 还内置了 rotate 操作.

示例中 1010, 1101 之类 看起来像二进制的均为二进制, 如果使用十进制会特殊说明

下方涉及到 1 0000 0101 之类的数字时, 1 为符号位

涉及到篇幅问题就不讲左移右移了, 有兴趣的小伙伴可以自己去查资料.

原码, 反码, 补码

在此之前我们需要了解一个概念: 机器数.

机器数是一个数的二进制形式在计算机中的表现形式, 他们分别是: 原码, 反码, 补码.

小伙伴看到这里可能会奇怪: "原码, 反码, 补码是什么, 为什么二进制要分成三种, 他们的表现形式又有什么不同"等种种疑惑.

首先我们都知道, 有整数和负数, 整数是很容易用二进制表示, 那负数呢? 计算机又看不懂 -, 于是科学家们又提出了有符号数这个概念, 用最高位来表示正负, 正数0, 负数1, 这种形式就叫原码.

// +2 的原码
0000 0010
// -2 的原码
1000 0010

反码: 正数不变, 负数将原码形式除了符号位其余皆取反

// -2 的原码
1000 0010
// +2 的反码
1111 1101

但是这两种形式运算都很麻烦啊, 于是就有了补码: 正数的补码就是其本身, 而负数的补码则是在其原码的基础上取反, 然后 + 1

// +2 的补码
0000 0010

// 原码负数 2
1111 1110

使用补码形式计算机可以直接对数字进行 加 运算, 有兴趣的小伙伴可以继续在深入查资料.

相互转换:

十进制值 原码 反码 补码
105 0 0110 1001 0 0110 1001 0 0110 1001
-105 1 1110 1001 1 1001 0110 1 1001 0111
正数不变, 负数符号位为 1 正数不变, 负数为除了符号位皆取反 正数不变, 负数为除了符号位皆取反然后 + 1

按位非 ~

按位非是一个特殊的运算符, 因为它是单目运算符, 而其他都是双目运算符, 按位非对每个 bit 执行 ~ 操作, 即取反码, 流程如下:

  1. 将用于计算的值转为原码形式 : ~1 => 0 0001

  2. 按位取反 : 0001 => 1 1110

  3. 将反码转换成补码 : 1110 => 1 0010

  4. 转换十进制 : 1 0010 => -2

示例如下:

         原码      反码       补码
      ~0 0001 => 1 1110 => 1 0010 => -2
等价于 ~1     => -2

相当于: 
~x = -(x + 1)

按位与 &

按位或对每个 bit 执行 & 操作, 如果相同位数的 bit 为 1, 则结果为 1, 示例如下:

      0001 & 1010 => 0000
等价于 1    & 10   => 0  

      0010 & 1010 => 0010
等价于 2    & 10   => 2


      x    & 0    = 0
      1111 & 0000 => 0000
等价于 15   & 0    => 0   

按位或 |

按位或对每个 bit 执行 | 操作, 如果相同位数的 bit 有一个为 1, 则结果为 1, 示例如下:

      0001 | 1010 => 1111
等价于 1    | 10   => 15   

      0010 | 1010 => 1010
等价于 2    | 10   => 10   

      x    | 0 = x
      1111 | 0000 => 1111
等价于 15   | 0    => 15   

按位异或 ^

按位异或对每个 bit 执行 ^ 操作, 如果相同位数的 bit 不相同, 且都不为 0, 则结果为 1, 示例如下:

      0001 ^ 1010 => 1011
等价于 1    ^ 10   => 11   

      0010 ^ 1010 => 1000
等价于 2    ^ 10   => 8   


      x    ^ 0    =  x 
      0001 ^ 0000 => 0001
等价于 1    ^ 0    => 1   

以及: 任何值同自身异或结果为 0
      0001 ^ 0001 => 0000
      1001 ^ 1001 => 0000
      1111 ^ 1111 => 0000

位掩码

上面终于讲完了位运算, 接着我们来请主角登场.

这个名词记得小伙伴们没有听过, 也可能已经用过了, linux 的权限系统就是使用位掩码来构建的, 当然可不要以为位掩码是一种技术, 它是一种运用位运算的技巧.

权限计算

我们在设计一个权限系统的时候往往免不了四种权限: 增删改查, 如果我们为每种有可能出现的权限都写一个变量来标识, 那我们需要写 2 ^ 4 = 16 个变量还储存这些组合.

这时就可以用位运算来优化, 首先我们用四个变量来表示权限:

const Insert = 1 // 0001
const Delete = 2 // 0010
const Update = 4 // 0100
const Select = 8 // 1000

比如我们想给一个用户添加 Insert 权限, 那怎么添加呢?

只需要用 |运算符:

const user = registerUser(data)

// 初始为 0000
user.premission = 0

// 如果想新增一个权限    0000 | 0001 => 0001, 此时就拥有了 Insert 的权限
user.premission = user.premission | Insert

// 如果想在增加一个权限  0001 | 0010 => 0011, 此时就拥有了 Insert,Delete 权限
user.premission = user.premission | Delete

// 如果想在增加一个权限  0011 | 0100 => 0111, 此时就拥有了 Insert,Delete,Update 权限
user.premission = user.premission | Update

此时肯定有小伙伴心里在想 0011 不是 3 吗, 我怎么知道 3 有没有包含 Insert 和 Delete 权限嘞?

只需要用 &运算符:

//         0011 & 0010  => 0010 也就是 Delete 的权限
user.premission & Delete === Delete
// 用户权限去 & 指定权限的结果等于指定权限, 那么就意味着该用户拥有该权限.

在用户有 Delete 和 Insert 的同时, 我们怎么取出其中的一个

只需要用 ^运算符:

//         0011 ^ 0010 => 0001
user.premission ^ Delete === Insert

如何删除权限?

需要用到 &~

// 有 Delete 和 Insert 权限
0011 & ~ 0001
     ↓                                           取反       补码    十进制
1. 先做 ~ 0001, 这里我们只到补码部分   ~ 0 0001 => 1 1110 => 1 0010 => -2
2. 转换 0011 到补码形式 -> 0 0011
3. 0011 & 0010 => 0010 => 2
user.premission = user.premission & ~ Insert === Delete

LoDash 的用法

这篇文章的初衷就是从阅读 lodash 源码衍生来的, 下面讲讲 lodash 中如何使用位掩码

在 lodash 的 cloneDeep 中用到了位掩码, lodash 的 baseClone 方法是诸多 clonexx 的基础, 那么 baseClone 势必要处理诸多状态, 例如是深克隆还是浅克隆, 克隆需不需要克隆 Symbol.

// 定义几种状态
const CLONE_DEEP_FLAG = 1
const CLONE_FLAT_FLAG = 2
const CLONE_SYMBOLS_FLAG = 4

function cloneDeep(value) {
  // cloneDeep 需要深克隆和克隆 Symbol  
  // 1 | 4 => 0001 | 0100 => 0101
  return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG)
}

function baseClone(value, bitmask, customizer, key, object, stack) {
  // 0101 & 0001 => 0001 => 之后用的时候 if(isDeep) 会隐式转换成 true
  const isDeep = bitmask & CLONE_DEEP_FLAG
  // 0101 & 0010 => 0000 => 之后用的时候 if(isFlat) 会隐式转换成 false
  const isFlat = bitmask & CLONE_FLAT_FLAG
  // 0101 & 0100 => 0100 => 之后用的时候 if(isFull) 会隐式转换成 true
  const isFull = bitmask & CLONE_SYMBOLS_FLAG
}

课外练习, 使用位运算的一些技巧

原地交换值

其技巧是利用

x = x ^ y

y = (x ^ y) ^ y

y = x ^ (y ^ y) : y ^ y = 0, x ^ 0 = x, 所以 y = x (参考按位异或篇提到的)

接下来算 x

x = (x ^ y) ^ (y ^ x ^ y)

x = x ^ y ^ y ^ x ^ y

x = y

let x = 2
let y = 3

// 0010 ^ 0011 => 0001  => 1
x = x ^ y

// 0011 ^ 0001 => 0010  => 2
y = y ^ x

// 0001 ^ 0010 => 0011  => 3
x = x ^ y

判断奇偶

其技巧就是判断二进制的最低位是不是 1

x = 2
// 为真则是偶数, 为假则是单数
x & 1 === 0

// 0010 & 0001 => 0000 => 0, x 是 偶数
x & 1 

//  x 赋值为 3
x = 3
// 0011 & 0001 => 0001 => 1, x 不是偶数
x & 1

交换符号

将 x 变成 -x, -x 变成 x

~ (x) + 1

笔者比较愚钝, 好多次才明白其中的意义, 本文主要是将其记录下来希望能给小伙伴们一些启发, 如有错误欢迎提 issue.

没有感情结束语: 本文同样发于个人博客