位运算实战技巧与性能优化应用指南

煜喆~ 优化 阅读 2,757
赞 19 收藏
二维码
手机扫码查看
反馈

又踩坑了,权限系统里一堆 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’])?性能上差多少?我还没实测过,但直觉上位运算应该快不少,毕竟纯数值操作。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论