你好,我是泰罗凹凸曼,今天我们来一篇JS中的位运算科普,经常在源码中看到的位运算符,和用其定义的一系列状态到底有什么优势?
位运算符号的基本了解
首先,我们应该要简单了解位运算符,常用的位运算符大概有以下几种,我们可以在JS中使用 toString
将数字转换为二进制查看,也可以通过 0b
开头来手动创建一个二进制数字:
(3).toString(2) // 11
0b00000011 // 3 前面的几位0可以省略,可以简写为 0b11
1. 与 &
按位对比两个二进制数,如果对应的位都为 1,则结果为 1,否则为 0
console.log((1 & 3) == 1) // true
对比图例如下所示:
2. 或 |
按位对比两个二进制数,如果对应的位有一个 1,则结果为 1,否则为 0
console.log((1 | 3) == 3) // true
对比图例如下所示:
3. 异或 ^
按位对比两个二进制数,如果对应的位有且只有一个 1,则结果为 1,否则为 0
console.log((1 ^ 3) == 2) // true
对比图例如下所示:
4. 非 ~
按位对操作的二进制数取反,即 1 变 0,0 变 1,任何数的非运算符
计算结果都是 -(x + 1)
const a = -1 // ~a = -(-1 + 1) = 0
console.log(~a) // 0
const b = 5 // ~b = -(5 + 1) = -6
console.log(~b) // -6
一个数和它的取反数相加的结果总为 -1
5. 左移 <<
左移会将二进制值的有效位数全部左移指定位数,被移出的高位(最左边的数字)丢弃,但符号会保留,低位(最右边的数字)会自动补0
console.log(1 << 2) // 4
图例如下所示:
6. 右移 >>
和左移相反的操作,将二进制的操作数右移指定位数,高位补0,低位丢弃!
console.log(4 >> 2) // 1
参考资料均来自 MDN,除了这些常用的符号之外,文档还标注了所有的JS操作符号,感兴趣的同学可以看一看!
有什么用?
说了这么多符号,对于操作符的影响是加深了,但是有什么用呢?二进制数字难理解,位操作符也难理解,二进制和十进制的互转不写个代码都心算不了,相信各位同学肯定有如此费解,我们先来看一段 Vue
的源代码,其中定义了很多状态类的字段!
源码位置戳这里
以及 Vue 中对其的使用,源码位置戳这里
我们可以看到,Vue 定义了一系列状态列标识一个 Dom 是属于什么类型,并用 VNode 中的一个字段 shapeFlag 来完成存储和判断,对状态的存储只用到了一个字段一个数字,就可以进行多种状态的判断!
我们尝试着设计一种类似的判断结构出来如何?
我有N个权限
假设系统中的用户我们规定其有增删改查
四个权限,我们可以设计一个枚举类来标识拥有的四个权限:
enum UserPerm {
CREATE = 1 << 0,
DELETE = 1 << 1,
UPDATE = 1 << 2,
SELECT = 1 << 3,
}
我们设计的时候,完全不必在意上述的二进制的十进制值是什么,只需要清楚的是,上述枚举的 1
在二进制位的哪个位置,如 1 的 二进制为 00000001
,将其左移 1(1 << 1), 就变成了 00000010
, 依次类推,我们用一个二进制串中的每一位来标识一个权限,这样一个字符串中只要出现对应位置的 1, 则该用户就拥有对应位置的权限,如图:
有什么好处呢?
我们知道二进制是可以转换为十进制的,这样子我们就可以用一个数字来表示多个权限,如一个用户完整的拥有四个权限,那他的二进制为 0b1111
, 那么其状态为数字 15
如果一个用户只有 CREATE
和 SELECT
的权限,那么二进制表达为 0b1001
,十进制数字为 9
后端数据库中,前端用户信息中,接口返回都只有一列一个字段就可以表示,那么用户信息应该是下面的形式:
const userInfo = {
name: '泰罗凹凸曼',
phone: '15888888888',
perm: 9, // 代表其只有 CREATE 和 SELECT 两种权限
}
权限的判断
如何判断这个用户是否具备某一个权限呢?那就需要请出我们的 与运算符(&),参考 Vue 的做法:
console.log(userInfo.perm & UserPerm.CREATE) // 9 & (1 << 0) = 1
console.log(userInfo.perm & UserPerm.UPDATE) // 返回 0, 0代表不通过
如果 userInfo.perm
中包含 CREATE
,就会返回 CREATE
的值,否则返回 0,在JS中,任何非0的数字都可以通过 if 判断,所以我们只需要一个判断就足够了!
if (userInfo.perm & UserPerm.CREATE) {
console.log('有创建权限')
} else {
console.log('没有创建权限')
}
什么原理?我们之前给过与运算符的图例,接下来我们看一下如上两句代码的图例所示:
我们看到,上下的符号位如果对不上的话,返回的结果都是 0,这样子我们就轻松实现了权限的判断
权限的增删
那么我们如何实现对一个用户的权限更新呢,比如给上面的用户新增一个 UPDATE
权限,这个时候我们就需要 或运算符(|)
比如:
userInfo.perm | UserPerm.UPDATE // 1001 | 0100 = 1101 = 13
这样子我们就对一个用户权限进行了增加,或的规则我们上面也给过图例,这里大家可以自己尝试理解一下,无非是两个二进制数 1001
和 0100
之间的或运算,只有其中一位为 1 则为 1,这两个数字计算的结果自然是 1101
那么如何实现权限删除呢?异或运算符(^)给你答案!有且只有一个 1,返回 1,否则为 0,删除对我们刚刚添加的 UPDATE
权限的方法:
userInfo.perm ^ UserPerm.UPDATE // 1101 ^ 0100 = 1001
非常简单是吧?看到这里,相信你已经完全理解位运算符在权限系统的妙用了,如果我这个时候需要添加一个新的权限,如分享权限,那么我只有用第五位的1来表示这个权限就可以啦
enum UserPerm {
SHARE = 1 << 5
}
// 添加分享权限
userInfo.perm | UserPerm.SHARE
以前的方案
我们以前在做用户标识的时候,通常会定义一个数组来表示,然后执行数组判断来进行权限的判断
const userPerm = ['CREATE', 'UPDATE', 'DELETE', 'SELECT']
// 判断有无权限
if (userPerm.includes('CREATE')) {
// ...
}
// 增加权限
user.perm.push('UPDATE')
// 删除权限
user.perm.splice(user.perm.indexOf('UPDATE'), 1)
相信大家也可以看出来,无论是从内存占用,效率,便捷程度来说位运算符的形式都是完胜,这也是会被各大项目使用的原因之一!快去你的项目中实践吧,记得写好注释哦!
结语
今天带大家认识了位运算符在权限系统的妙用,小伙伴们还有什么使用位运算符的巧妙思路,可以在评论中给出来哦!继续加油吧,快去实践少年!
祝大家越来越牛逼!
去探索,不知道的东西还多着呢,我是泰罗凹凸曼,M78星云最爱写代码的,我们下一篇再会!
原文链接:https://juejin.cn/post/7244809939838844984 作者:泰罗凹凸曼