- [时间]: 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 => 0 0001
-
按位取反 : 0001 => 1 1110
-
将反码转换成补码 : 1110 => 1 0010
-
转换十进制 : 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.
没有感情结束语: 本文同样发于个人博客