位运算实战技巧与性能优化应用指南
又踩坑了,权限系统里一堆 flag 判断卡到爆
上周改一个老项目,用户权限那块逻辑写得跟意大利面条似的。每个功能点都要判断用户有没有某个权限,比如“能不能导出数据”“能不能删除评论”,代码里全是 if (user.canExport && user.isAdmin && !user.isGuest) 这种,看得我脑壳疼。更烦的是,产品经理突然说要加个“只读编辑”角色,权限组合一下子多了一倍,改起来简直要命。
本来想硬着头皮一个个 if 改,但想到之前看过别人用位运算搞权限控制,就琢磨着试试。结果一开始完全搞错了方向——我以为只要把权限值设成 1、2、4、8 就行,然后用 & 判断。但实际一跑,发现某些组合判断会误判。折腾了半天才发现,是我对位掩码的理解太浅了。
这里我踩了个大坑:权限值必须严格按 2 的幂次分配
一开始我把几个权限随便赋值:
const PERMISSIONS = {
READ: 1,
WRITE: 2,
DELETE: 3, // ❌ 错了!3 不是 2 的幂
EXPORT: 4
};
然后判断用户是否有 DELETE 权限时用了 user.permissions & PERMISSIONS.DELETE。问题来了:如果用户权限是 1(只有 READ),1 & 3 结果是 1,非零,JS 里算 true——明明没 DELETE 权限,却被判定为有!
后来试了下发现,DELETE 必须设成 4(如果前面用了 1 和 2),或者统一按位分配:
const PERMISSIONS = {
READ: 1 << 0, // 1
WRITE: 1 << 1, // 2
DELETE: 1 << 2, // 4
EXPORT: 1 << 3, // 8
ADMIN: 1 << 4 // 16
};
这样每个权限独占一位,互不干扰。判断的时候,只要目标权限对应的位是 1,& 的结果就非零;否则就是 0。这才对。
核心代码就这几行,但细节很多
最终我封装了一个小工具类,用起来清爽多了:
class PermissionChecker {
constructor(userPermissionBits) {
this.bits = userPermissionBits;
}
// 检查是否拥有全部指定权限
has(...permissions) {
const required = permissions.reduce((acc, p) => acc | p, 0);
return (this.bits & required) === required;
}
// 检查是否拥有任一指定权限
hasAny(...permissions) {
const required = permissions.reduce((acc, p) => acc | p, 0);
return (this.bits & required) !== 0;
}
// 添加权限(返回新实例,保持不可变)
grant(...permissions) {
const newBits = permissions.reduce((acc, p) => acc | p, this.bits);
return new PermissionChecker(newBits);
}
// 移除权限
revoke(...permissions) {
const mask = permissions.reduce((acc, p) => acc | p, 0);
const newBits = this.bits & ~mask;
return new PermissionChecker(newBits);
}
}
用法很简单:
const userPerm = new PermissionChecker(5); // 二进制 101,即 READ + DELETE
console.log(userPerm.has(PERMISSIONS.READ, PERMISSIONS.DELETE)); // true
console.log(userPerm.has(PERMISSIONS.WRITE)); // false
console.log(userPerm.hasAny(PERMISSIONS.WRITE, PERMISSIONS.EXPORT)); // false
console.log(userPerm.hasAny(PERMISSIONS.READ, PERMISSIONS.WRITE)); // true
// 动态加权限
const newUserPerm = userPerm.grant(PERMISSIONS.WRITE); // 现在有 READ+WRITE+DELETE (1+2+4=7)
这里注意我踩过好几次坑:判断“拥有全部权限”时,不能只看 & 是否非零,必须等于 required 值本身。比如用户权限是 7(111),required 是 5(101),7 & 5 = 5,等于 required,说明两个位都有;但如果 required 是 6(110),7 & 6 = 6,也成立。但如果用户权限是 5(101),required 是 6(110),5 & 6 = 4,不等于 6,所以返回 false——这才是正确的。
后端怎么存?其实很简单
前端搞定后,还得和后端对齐。我们后端是 Node.js,数据库里直接存一个整数字段 permission_bits。用户登录时,接口返回这个数字就行:
{
"userId": 123,
"permission_bits": 13
}
13 的二进制是 1101,对应 READ (1) + WRITE (4) + EXPORT (8) —— 注意 DELETE 是 4,这里没包含,所以 13 = 8+4+1。
后端赋权限时也用同样逻辑。比如给用户加 ADMIN 权限(16):
// 假设原权限是 13
const newPermission = 13 | 16; // 29
删权限:
const newPermission = 29 & ~16; // 13
亲测有效,而且数据库查询还能用位运算过滤,比如找所有有 EXPORT 权限的用户:
SELECT * FROM users WHERE permission_bits & 8 = 8;
不是万能的,但够用了
当然,位运算权限也有局限。比如权限种类超过 32 种时,JavaScript 的 Number 是双精度浮点,虽然能表示整数到 2^53,但位运算操作会转成 32 位有符号整数,高位会丢。这时候就得换 BigInt 或者拆成多个字段。不过我们项目目前就 8 个权限,远不到这程度。
另外,这种方案不适合需要动态增删权限类型的场景——因为权限值是硬编码的。但对我们这种权限结构稳定的后台系统,改起来反而比维护一堆布尔字段省事多了。
上线后,原来几十行的权限判断缩成了两三行,代码清晰不少。虽然第一次用位运算有点懵,但搞懂之后真香。现在加新角色,只要算好对应的位组合,一行配置搞定。
以上是我踩坑后的总结,如果你有更好的方案欢迎评论区交流。比如有没有人用过基于字符串集合的权限(像 [‘read’, ‘write’])?性能上差多少?我还没实测过,但直觉上位运算应该快不少,毕竟纯数值操作。

暂无评论