JavaScript位运算及其妙用

​ 现代计算机的数据是以二进制的形式进行存储的。 位运算就是直接对整数在内存中的二进制位进行操作。使用位运算可以提高代码性能,精简代码。

​ 在说位运算前,先简单说说几个概念,具体有机器数、真值、原码、反码、补码

机器数

一个数在计算机中的二进制表示形式,叫做这个数的机器数。

机器数是带符号的,计算机中机器数的最高位是符号位, 正数为0, 负数为1。

二进制的位数受机器设备的限制。机器内部设备一次能表示的二进制位数叫机器的字长,一台机器的字长是固定的。字长8位叫一个字节(Byte),机器字长一般都是字节的整数倍,如字长8位、16位、32位、64位。

例如在8位机器中,+3转为二进制为 00000011,-3转为二进制为 10000011,其中00000011和10000011就是机器数。

机器数的真值

直接用正号"+"和负号"-"来表示其正负的二进制数叫做机器数的真值。

例如"00000011"和"10000011"是两个机器数,而它们的真值分别为+0000011(即+3)和-0000011(即-3)。

机器数包含原码、反码和补码三种表示形式

原码

将数的真值形式中"+"号用"0"表示,"-"号用"1"表示时,叫做数的原码形式,简称原码。

也可理解为符号位加上真值的绝对值。第一位是符号位,其余位表示数值。

例如:

十进制 真值 原码
+3 +0000011 00000011
-3 -0000011 10000011

原码的表示比较直观,但是在进行加减法运算时,由于符号位的存在,使得机器的运算比较复杂,所以就引入了反码和补码。

反码

对于正数来说,其反码和原码完全相同。

对于负数来说,其反码是在其原码的基础上, 符号位不变,其他位取反(0变1,1变0)。

十进制 真值 原码 反码
+3 +0000011 00000011 00000011
-3 -0000011 10000011 11111100

补码

对于正数来说,其补码和原码完全相同。

对于负数来说,其补码是其反码的末位加1,也就是说在其原码的基础上, 符号位不变,其他位取反后加1。

十进制 真值 原码 反码 补码
+3 +0000011 00000011 00000011 00000011
-3 -0000011 10000011 11111100 11111101

注意:在计算机系统中,数值一律用补码来表示和存储。主要原因是为了便于处理和运算。

具体为何需要引入反码和补码以及为何计算机系统中数值用补码来表示和存储,文章篇幅有限,此块也不是本文的重点,详细可以参考这篇文章www.cnblogs.com/zhangziqiu/...

位运算符

​ 在 JavaScript 中,数值是以64位浮点数的形式储存。但是位运算符只对整数起作用。JavaScript在做位运算时,会事先将其转换为32位有符号整型(用比特序列表示,即0和1组成)并开始计算,在得到结果后再将其转回JavaScript的数值类型。

​ 因为 js 的整数默认是带符号数,所以在位运算中,只能使用 31 位,开发者是不能访问最高位的。在运算时,如果位数超过,那么超过的数字会被丢弃,这会造成各种错误的结果,并且没有任何警告信息。所以,并不是所有计算都适合通过位运算符来计算。

​ 位运算符操作的是数值对应的机器数,这个机器数的表示形式就是补码的形式。(上面说了,为了便于处理和运算,计算机系统中,数值用补码来表示和存储。)

​ JavaScript中的位运算符有以下:

  • & :与运算;两个运算比较的bit位都为1时,这个bit位才为1,只要有任意一个位是0,这个位就是0

    javascript 复制代码
    console.log(5 & 3) // 输出1
    // 5的机器数表示为 00000000 00000000 00000000 00000101
    // 3的机器数表示为 00000000 00000000 00000000 00000011
    // 所以计算结果为1 00000000 00000000 00000000 00000001
  • | :或运算;两个运算比较的bit位只要有一个是1,这个bit位就是1

    javascript 复制代码
    console.log(5 | 3) // 输出7
    // 5的机器数表示为 00000000 00000000 00000000 00000101
    // 3的机器数表示为 00000000 00000000 00000000 00000011
    // 所以计算结果为7 00000000 00000000 00000000 00000111
  • ^ :异或运算;两个运算比较的bit位相同则为0,不同则为1

    javascript 复制代码
    console.log(5 ^ 3) // 输出6
    // 5的机器数表示为 00000000 00000000 00000000 00000101
    // 3的机器数表示为 00000000 00000000 00000000 00000011
    // 所以计算结果为6 00000000 00000000 00000000 00000110
  • ~:取反运算;0变1,1变0

    javascript 复制代码
    console.log(~5) // 输出-6
    // 5的机器数表示为	00000000 00000000 00000000 00000101
    // 取反之后表示为	11111111 11111111 11111111 11111010
    // 该数是负数,负数补码与原码不同,先将其转为原码的反码:11111111 11111111 11111111 11111001
    // 再转为原码:10000000 00000000 00000000 00000110
    // 所以结果为:-6
  • << :左移;各二进位全部左移若干位,高位丢弃(即左边超出的丢弃),低位补0(即右边差的)

    javascript 复制代码
    console.log(5 << 3) // 输出40
    // 5的机器数表示为  00000000 00000000 00000000 00000101
    // 左移三位结果为40 00000000 00000000 00000000 00101000

    图示:

    javascript 复制代码
    console.log(-1073741822 << 2) // 输出8
    // -1073741822的机器数表示为	 11000000 00000000 00000000 00000010		
    // 左移两位结果为8            00000000 00000000 00000000 00001000

    图示:

  • >> :带符号扩展右移;各二进位全部右移若干位,右边被移出的丢弃,左边会复制最左侧的位来填充左侧(这里最左侧的位是符号位,也可以认为对于无符号数,高位补 0,对于有符号数,高位补符号位)。

    javascript 复制代码
    console.log(5 >> 2) // 输出1
    // 5的机器数表示为	          00000000 00000000 00000000 00000101
    // 带符号扩展右移两位结果为1     00000000 00000000 00000000 00000001

    图示:

    javascript 复制代码
    console.log(-5 >> 2) // 输出-2
    // -5的机器数表示为:         11111111 11111111 11111111 11111011
    // 带符号扩展右移两位结果为:   11111111 11111111 11111111 11111110
    // 先将该值转为原码的反码:    11111111 11111111 11111111 11111101
    // 再转为原码:		10000000 00000000 00000000 00000010
    // 所以结果为:-2

    图示:

  • >>> :无符号右移;各二进位全部右移若干位,右边被移出的丢弃,左边用0填充。因为符号位变成了 0,所以结果总是非负数。(即便右移 0 个比特,结果也是非负的。)

    javascript 复制代码
    console.log(5 >>> 2) // 输出1
    // 5的机器数表示为	      00000000 00000000 00000000 00000101
    // 无符号右移两位结果为1    00000000 00000000 00000000 00000001

    图示:

    javascript 复制代码
    console.log(-5 >>> 2) // 输出1073741822
    // -5的机器数表示为:                11111111 11111111 11111111 11111011
    // 无符号右移两位结果为1073741822:    00111111 11111111 11111111 11111110

位运算符的妙用

位运算判断奇数偶数

根据数值的机器数最后一位是0还是1即可判断,为0就是偶数,为1就是奇数。

javascript 复制代码
function fun(num) {
  if ((num & 1) === 0) {
    return "偶数";
  }
  if ((num & 1) === 1) {
    return "奇数";
  }
}
console.log(fun(44)); // 输出偶数
console.log(fun(55)); // 输出奇数

位运算实现两数交换

不使用第三个变量,实现两个数交换,可以通过以下方式实现

javascript 复制代码
a = a + b;
b = a - b;
a = a - b;
// 或者
a = [b, (b = a)][0];
// 或者
[a, b] = [b, a]

根据异或运算的规则,可以得出一些特性

javascript 复制代码
// a取任何值,下面的结果都成立
a^a = 0
a^0 = a
// 异或运算且满足交换律和结合律,即
a^b = b^a
a^b^c = a^(b^c) = (a^c)^b
// 其实与运算也是满足交换律和结合律的
a&b = b&a
a&b&c = a&(b&c) = (a&c)&b
// 于是就可以推出
a^b^a = b^(a^a) = b^0 = b
// 如果 c = a^b 那么 a = c^b,这是因为 a = c^b = a^b^b = a^(b^b) = a^0 = a
// 如果 d = a^b^c 那么 a = d^b^c, 以此类推...

那么根据以上的特性,就可以用位运算的方式实现两数交换,如下

javascript 复制代码
a = a^b
b = a^b
a = a^b

位运算实现乘除法

将某个数值带符号右移一位,相当于该数值除以2;

将某个数值左移一位,相当于该数值除以2;

javascript 复制代码
console.log(6 >> 1) // 输出3
console.log(6 << 1) // 输出12

而右移n位,就相当于该数值除以2的n次方

而左移n位,就相当于该数值乘以2的n次方

javascript 复制代码
console.log(32 >> 3) // 输出4, 相当于32除以2的3次方(8),等于4
console.log(32 << 3) // 输出256, 相当于32乘以2的3次方(8),等于256

但是用这种用法实现乘除法有很多问题,比如

  • 结果只会保留整数部分

    javascript 复制代码
    console.log(7 >> 1) // 输出3
  • 如果数值操作影响到了符号位,结果会错误

    javascript 复制代码
    console.log(-2147483646 << 1) // 输出4, 而-2147483646除以2的结果应该是-1073741823

位元算实现取整

  • 通过或运算实现:将数值与0进行或运算从而得到该数值的整数部分

    javascript 复制代码
    console.log(5.2 | 0) // 输出5
    console.log(-7.2 | 0) // 输出-7
  • 通过与运算实现:将数值与-1进行或运算从而得到该数值的整数部分

    javascript 复制代码
    console.log(5.2 & -1) // 输出5
    console.log(-7.2 & -1) // 输出-7
  • 通过异或运算实现:

    • 将数值与0进行异或运算

      javascript 复制代码
      console.log(5.2 ^ 0);// 输出5
      console.log(-7.2 ^ 0);// 输出-7
    • 将数值与另一个数进行两次异或运算

      javascript 复制代码
      console.log(5.2 ^ 2 ^ 2); // 输出5
      console.log(-7.2 ^ 2 ^ 2);// 输出-7
  • 通过取反运算实现:将数值进行两次取反运算

    javascript 复制代码
    console.log(~~5.2); // 输出5
    console.log(~~-7.2); // 输出-7
  • 通过左移或右移运算实现:将数值左移或右移0位

    javascript 复制代码
    console.log(5.2 >> 0); // 输出5
    console.log(-7.2 << 0); // 输出-7

位掩码和权限管理

位掩码(BitMask),是"位(Bit)"和"掩码(Mask)"的组合词。"位"指代着二进制数据当中的二进制位,而"掩码"指的是一串用于与目标数据进行按位操作的二进制数字。组合起来,就是"用一串二进制数字(掩码)去操作另一串二进制数字"的意思。

位掩码可以用于多种场景,比较常用的,我们可以用它来进行权限管理。

假设我们在做一个直播教学系统,每个学生都有其自己的状态,比如是否举手,摄像头状态,设备类型三个状态。

如果我们给每个学生设置三个状态值,那会很麻烦(这里只是举例三个,现实场景可能有七八个状态值)

那么我们可以设计每个学生一个状态码,例如这个状态码是一个9位的二进制数

我们可以这个数上划分每个状态所占的位

例如从最低位(按最低位0)开始

  • 低二位(从最低位(0)到第1位)表示学生的举手状态。举手的话为 01,未举手为 10,未知为 00
  • 从低位第二位到第四位表示该学生的摄像状态。打开为001,关闭为010,未知为000,设备故障为011,被禁用为100
  • 从低位第五位到第八位表示该学生的设备类型。例如window是0001,mac是0010,ipad是0011,android是0100,ios是0111,未知是0000

具体可以在代码中定义一个枚举:

javascript 复制代码
const enums = {
  // 举手状态
  handsUp: {
    open: 0b01, // 举手中
    close: 0b10, // 未举手
    unknow: 0b00, // 未知
    lowBitPos: 0, // 从第几位开始使用
    bitLength: 2, // 所占位的长度
  },
  // 摄像头状态
  camera: {
    open: 0b001, // 打开
    close: 0b010, // 关闭
    disable: 0b011, // 设备故障
    ban: 0b100, // 被禁用
    unknow: 0b000, // 未知
    lowBitPos: 2, // 从第几位开始使用
    bitLength: 3, // 所占位的长度
  },
  // 设备类型
  device: {
    window: 0b0001, // window电脑
    mac: 0b0010, // 苹果电脑
    ipad: 0b0011, // 苹果平板
    android: 0b0100, // 安卓手机
    ios: 0b0111, // 苹果手机
    unknow: 0b0000, // 未知
    lowBitPos: 5, // 从第几位开始使用
    bitLength: 4, // 所占位的长度
  }
};

例如我随便举个例子,假设某个学生的状态码为 0b001001001

那么其对应状态如下

但是在代码中,我们要如何获取或修改学生状态对应的值呢?这就要用到前面所说的位掩码的知识了

获取

首先是获取状态值,我们需要从该状态码中截取出指定的从某一位到某一位的值。

javascript 复制代码
// 举个例子,假设学生此时的状态码为0b001001001,获取该学生的摄像头状态
let state = 0b001001001
// 因为摄像头状态是从低位开始(0开始)第二位到第四位
// 首先将状态码右移两位
let v = state >> enums.camera.lowBitPos  // enums上面定义了
// 然后将转换后的值与0b111进行与运算
let result = v & 0b111
console.log(result) // 输出结果为3,也就是0b010,如此便获取到了该学生的摄像头状态

上面代码中,因为摄像头状态是占了三个bit位,所以需要与0b111进行与运算来得到结果,但是举手状态是两位,设备类型是四位,就要分别和0b11和0b1111进行与运算,这样比较麻烦,所以上面的代码可以优化为

javascript 复制代码
let result = v & ((0b1 << enums.camera.bitLength) - 1)

也可以将所有需要用到的位全是1的值先存在数组里面,方便后续运算

javascript 复制代码
const bitArr = [0b0]
// 但位运算时仅支持 1 << 31大小
for (let i = 1; i < 32; i++) {
    bitArr.push((0b1 << i) - 1)
}
Object.freeze(bitArr) // 冻结数组  不允许修改

let result = v & bitArr[enums.camera.bitLength]

总结上面代码,我们可以将获取状态值封装为一个函数

javascript 复制代码
const getProvide = (state, lowBitPos, bitLength) => {
   return (state >> lowBitPos) & bitArr[bitLength]
}

修改

知道如何获取以后,那么修改该如何呢

例如,目前学生的状态码对应的设置状态为0b0010(苹果电脑),我们要修改学生的设备状态为0b0100(安卓手机)

javascript 复制代码
let state = 0b001001001
// 首先先获取设备状态对应的值
let deviceState = getProvide(state, enums.device.lowBitPos, enums.device.bitLength)
// 然后先清除设备状态对应的位数区间
let clearState = state - (deviceState << enums.device.lowBitPos)
// 然后重新加上新的状态区间即可
let result = clearState + (enums.device.android << enums.device.lowBitPos)
console.log(result.toString(2)) // 输出 010001001

封装为函数如下

javascript 复制代码
const setProvide = (state, lowBitPos, bitLength, newSingleState) => {
  const get = getProvide(state, lowBitPos, bitLength)
  let clearState = state - (get << lowBitPos)
  return clearState + (newSingleState << lowBitPos)
}

如此便实现了对学生用户的权限状态控制。

相关推荐
GIS程序媛—椰子8 分钟前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
DogEgg_00115 分钟前
前端八股文(一)HTML 持续更新中。。。
前端·html
ZL不懂前端17 分钟前
Content Security Policy (CSP)
前端·javascript·面试
乐闻x21 分钟前
ESLint 使用教程(一):从零配置 ESLint
javascript·eslint
木舟100921 分钟前
ffmpeg重复回听音频流,时长叠加问题
前端
王大锤439132 分钟前
golang通用后台管理系统07(后台与若依前端对接)
开发语言·前端·golang
我血条子呢1 小时前
[Vue]防止路由重复跳转
前端·javascript·vue.js
黎金安1 小时前
前端第二次作业
前端·css·css3
啦啦右一1 小时前
前端 | MYTED单篇TED词汇学习功能优化
前端·学习