一、前言
位运算, 平常工作中用的比较少, 但是不代表不重要, 像 React 源码中的 Lane 模型就涉及到位运算的内容, 很多涉及到此的学习资料,通常讲的一笔带过, 经常不知道是为什么, 今天我们就通过设计一个小的权限控制系统, 学习一下位运算的基本使用
二、基础知识
1、 为什么 0.1 + 0.2 算不准?
面试经常问 0.1 + 0.2 为什么算不准, 怎么解决 其实这个问题就和我们今天的学习有关, 0.1 和 0.2 我们都知道是十进制的浮点数,转化为二进制时是无限循环的小数, 而JS使用的IEEE 745标准下, 二进制数字是有范围的 在 -(2^53 -1) 至 2^53 -1 之间,所以超出的部分会丢失, 所以在计算后, 0.1+0.2 !== 0.3, 解决方式有很多种 如:
-
- tofixed : (0.1+0.2).toFixed(1) 通过四舍五入修正
-
- 浮点数转成整数计算后算回: (0.110+0.210)/10
-
- 通过转成字符串然后经过一定规则计算,一些库就是这样实现的
-
- 使用 math.js 库等
2、学一下进制的表示
我们经常使用十进制, 对其他进制用的比较少,不同进制之间的计算通常由JS内部转化处理, 然后输出十进制,接下来看一下不同进制的表示
// 十进制
123456
// 二进制 0b 0B开头
0b1 //1
// 八进制 0o 0O
0o755 // 493
// 十六进制 color 颜色值的表示经常使用
0xA // 10
toString 方法默认转化成10进制, 内部可以通过数字转化为其他进制
let binary = (1).toString(2);
console.log(binary); // 输出: "1" 这里的 1 是二进制的1 也就是 0b1
3、位运算的方法
按位操作符在 JS 中会操作当作32位比特序列进行操作, 可以理解为内部会将操作数转换成二进制形式, 并对每一位进行操作
下面是常见的按位操作符
-
- 按位与 (&) 两个操作数进行AND操作, 两个操作数对应位置上都是1,结果为1,否则为0
-
- 按位或(|) 两个操作数的每一位进行异或操作, 对应位置至少有一个1,结果就是1,否则为0
-
- 按位异或(^) 两个操作数对应位置上值相等,结果为0,否则为1 (简化记忆方式: 当两个操作数相应的比特位有且只有一个 1 时,结果为 1,否则为 0 )
-
- 按位非 (~)对操作数的每一位进行取反, 0 变 1 、 1 变 0 转成有符号整数
一看很蒙, 分别举例说明 按位或(|)
let a = 5
let b =3
let resultAnd = 5 | 3 // 输出 7
解释:
先分别转化为32位的二进制, 5 => (5).toString(2) => '101'
同理 3 =>(3).toString(2) => '11'
计算过程如下: 位数不够补0
101
011
111
按位或是有一位为1就为1, 所以计算结果为二进制 111
转成十进制 (0b111).toString(10) , 结果为7
按位与(&)
let a = 5
let b =3
let resultAnd = 5 & 3 // 输出 1
仍然转化成二进制, 对齐, 位数不够补0 ,计算格式如下, 按位与是两个都为1才为1,否则为0
101
011
001
输出结果为001 转为十进制 (0b001).toString() => 1
按位异或( ^ )
在来一个按位异或
let a = 5
let b =3
let resultAnd = 5 ^ 3 // 输出 6
通过是上述方式计算,位数不够补0 , 方便记忆的规则是仅有一位1时,结果为1,否则为0
101
011
110
按这个规则,我们输出的是二进制 010 转成十进制是 (0b110).toString() => 6
按位非(~)
let a = 5
~a => -6
这里就很费劲, 为什么会这样呢?
按我么之前的 a.toString(2) 为二进制 101
然后按规则转化取反
101
=>
010
结果应该是-010啊, 转成二进制,应该是-8, 可结果为什么是 -6呢 ,-6 的二进制 是 -110, 看起来不太对劲, 其实是忽略了一条, 操作数转换为的是32位的二进制表示, 不够的位数通过0补码, 我们之前的操作符因为两两对齐, 通常不需要补齐到32位, 但是按位非,对自身的取反操作就需要考虑全体值了, 正确的转化应该是这样的
(5).toString(2).padLeft(32,'0')
取反
00000000000000000000000000000101
11111111111111111111111111111010
然后把11111111111111111111111111111010转化成有符号整数, 有符号整数是个重难点, 可以详细看转码, 结果 为 -6
三、权限设置
上面的基础知识就完全可以让我们设计一个会员权限了, 会员权限要有一定的约束,比如权限码唯一, 增加删除权限简单, 校验简单
接下来我们简单模拟读写修改删除四个权限, 通过操作符解决上面的问题
const READ = 0b1; // 0001 => 1
const CREATE = 0b10; // 00010 => 2
const UPDATE = 0b100; // 000100 => 4
const DELETE = 0b1000; // 0001000 => 8
我们通过四个变量, READ、CREATE、UPDATE、DELETE 分别代表了读、 写、 修改、 删除, 并且附了一个二进制的值
1、 增加权限
给用户赋不同权限可以用按位或 比如给用户设置 读、写、更新权限
const user = READ | CREATE | UPDATE // => 7
2、验证权限
验证权限 我们通过 按位与 来判断
const user = READ | CREATE | UPDATE // => 7
方式一 通过0判断, 存在权限结果一定不为0
console.log(user & READ) // 1
console.log(user & DELETE) // 0
方式二
console.log((user & READ) === READ) // true
console.log((user & DELETE) === DELETE) // false
3、删除权限
删除权限有两种方式 按位异或 或者 按位非
1、 使用按位异 或 按位非
方法一
const user = READ | CREATE | UPDATE
const user2 = user ^ READ // 6
(user2 & READ) === READ //=> false
按位异 可以理解为 toggle 当内部有的时候,可以删除, 没有权限的时候是增加权限, 如在上面基础上我们再次 user ^ READ (user2 & READ) === READ 就为 true 了, 如果只想让其删除,可以配合上面的查询,有在执行,或者用下面的方法
方法二: 按位与 与要删除的权限取反
const user = READ | CREATE | UPDATE
const user2 = user & (~CREATE) // 5
(user2 & READ) === READ //=> true
(user2 & CREATE) === CREATE //=> false
至此, 对用户的权限的增删改查,其实已经全部完成了, 可以封装成方法, 通过不同的用户角色进行授权, 校验
三、总结
通过位运算的操作, 我们可以很容易的,对用户权限进行操作和处理, 很多后端都是通过这样处理的, 二进制的处理具有存储空间小, 传输数据量小的特点, 对与权限类的操作非常合适。