当前位置: 首页 > 工具软件 > 8Bit.js > 使用案例 >

关于JS中number位(Bit)操作的一些思考

呼延卓
2023-12-01

  JavaScript中所有的数字都按照IEEE-754标准储存为64位。但是位操作符却是转换为32位数字进行操作的。关于这一点的可用性进行了一些思考。

32带符号整数

  先来复习一下32带符号整数的数据表示方式。在计算机中,所有的数据都是被储存为0和1的序列,数字也不例外。比如数字4,通过二进制转换,储存为100。但是整数在实际业务场景中是存在正负的,如何用01序列来表示一个负数呢。我们可以拿出一个位置,用0或者1来标志这个数字是整数还是负数。

  在JS的32带符号整数中,数字从左边的第一位用于标识正负数(1表示负数,0表示整数)。在用二进制转换完毕后,用0补齐不足32位的部分。比如数字8的二进制为1000。我们先用0补齐到31位0000000000000000000000000001000,然后在左边第一位放上表示正负的位数,8是正数放上0,因此8在32带符号整数中为00000000000000000000000000001000

  而如果我们要表示一个负数,比如-18,我们需要先找到该负数的绝对值1832带符号整数中的01序列,为00000000000000000000000000010010,然我们把所有位上面的数字取反,转为11111111111111111111111111101101。然后我们把它加上1,得到11111111111111111111111111101110就是18在32带符号整数中的表示。我们可以在控制台中验证一下:

console.log(0b11111111111111111111111111101110 | 0); // -18
0b11111111111111111111111111101110; // 4294967278

  在这里0b起头表示后续的数字是以二进制方式来储存。|位操作,表示同等级位上有1个以及以上的1为1,否则为0,因此| 0表示为取原数值。但是因为位操作符建立在32带符号整数的规则上,这里| 0实际上的含义是把前面的数值转换为32带符号整数。因此我们看到我们直接运算0b11111111111111111111111111101110得到的结果是4294967278而不是-18

  位操作符是建立在32位带符号整数操作上进行的,我们还可以通过最大值的方式很简单的证明。

const _32_SIGNAL_BIT_MAX_NUMBER = Math.pow(2, 32 - 1) - 1; // 2147483647
_32_SIGNAL_BIT_MAX_NUMBER | 0; // 2147483647
_32_SIGNAL_BIT_MAX_NUMBER + 1 | 0; // -2147483648

  32位带符号整数,由于数字的第一位用于标识正数/负数的,它所能表示的最大值为2的31次方-1,即2147483647。而数字2147483648在二进制的表示是10000000000000000000000000000000,由于第一位在32位带符号整数用于标识正负数,1为负数,因此2147483647 | 0转换之后会变为负数的-2147483648

位操作的一些思考

利用&判断奇偶

  与操作符&是在两个数的二进制同位都为1时才会得到1的操作符。当使用& 1操作的时候,实际上就是得到被操作数的最后一位二进制是0还是1的一个操作。

/*
  操作
  00000000000000000000000000010010
                                 & 
                                 1
*/
18 & 1; // 0

  而在二进制中,所有的奇数末尾都是1,因为2进制中不存在数字2,偶数势必进一到下一位上了。所以可以利用& 1操作来判断被操作数的奇偶。

const isOdd = number => !!(number & 1);

利用|取整

  前面有讨论过,任意数| 0得到的是数的原值,但是位操作是基于32位带符号整数的运算,因此被操作数会被转为32位带符号整数,这意味着被操作数的浮点数部分会统一被丢失,同时在大于2147483648小于4294967296部分由于第一位是1会变转为负数,而在大于4294967296的数字上,会丢失位数超过32部分。因此使用| 0操作,可以在小于2147483648处进行求整的操作。

使用|&结合的打标模式

  在阅读react源码的时候,代码中多次使用了|&的打标模式,来快速标志一个变量是否拥有某一种状态。我们在平常的业务场景中其实很容易遇到多种状态的场景,比如一个账号登陆拥有多种权限。此时我们一般会使用数组来进行操作。

// @enum {string} admin/user/development
const ADMIN_ROLE_TYPE = 'admin';
const USER_ROLE_TYPE = 'user';
const DEVELOPMENT_ROLE_TYPE = 'development';

// 某个业务场景的roleType
let exampleRoleType = [ADMIN_ROLE_TYPE, USER_ROLE_TYPE];

// 判断是否拥有 development 权限
exampleRoleType.includes('development');
// 添加 development 权限
exampleRoleType = exampleRoleType.includes('development') ? exampleRoleType : [...exampleRoleType, DEVELOPMENT_ROLE_TYPE];
// 删除 development 权限
exampleRoleType = exampleRoleType.filter(roleType => roleType !== 'development');

  比如在上面的场景中,账号权限有三种情况adminuser以及development,且这三种角色权限可以累加。当我们标识一个账户的roleType的时候通常使用数组进行维护角色权限的增删改查。比如我们需要给一个账号添加某个权限的时候,我们首先需要判断roleType数组中是否存在该角色,如果不存在,再把这个角色添加进数组。

  而利用位操作,我们只需要把roleType维护为一个数字即可拥有多种权限。

// @enum {string} admin/user/development
const ADMIN_ROLE_TYPE = 0b001;
const USER_ROLE_TYPE = 0b010;
const DEVELOPMENT_ROLE_TYPE = 0b100;

let exampleRoleType = ADMIN_ROLE_TYPE | USER_ROLE_TYPE;

// 判断是否拥有 development 权限
exampleRoleType & DEVELOPMENT_ROLE_TYPE;
// 添加 development 权限
exampleRoleType |= DEVELOPMENT_ROLE_TYPE;
// 删除 development 权限
exampleRoleType ^= DEVELOPMENT_ROLE_TYPE;

  这种模式很巧妙的让每一种权限类型用二进制上面不同位的1来标识,然后使用位操作符在不同的位上面储存或者删除权限。让原来数组形式维护的增删改查复杂度为O(n)的数据结构成功降为O(1)

 类似资料: