百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

JavaScript位运算及其妙用(js编写位置)

toyiye 2024-07-06 23:48 35 浏览 0 评论

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

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

机器数

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

机器数是带符号的,计算机中机器数的最高位是符号位, 正数为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
console.log(5 & 3) // 输出1
// 5的机器数表示为 00000000 00000000 00000000 00000101
// 3的机器数表示为 00000000 00000000 00000000 00000011
// 所以计算结果为1 00000000 00000000 00000000 00000001

:或运算;两个运算比较的bit位只要有一个是1,这个bit位就是1

console.log(5 | 3) // 输出7
// 5的机器数表示为 00000000 00000000 00000000 00000101
// 3的机器数表示为 00000000 00000000 00000000 00000011
// 所以计算结果为7 00000000 00000000 00000000 00000111

^ :异或运算;两个运算比较的bit位相同则为0,不同则为1

console.log(5 ^ 3) // 输出6
// 5的机器数表示为 00000000 00000000 00000000 00000101
// 3的机器数表示为 00000000 00000000 00000000 00000011
// 所以计算结果为6 00000000 00000000 00000000 00000110

~:取反运算;0变1,1变0

console.log(~5) // 输出-6
// 5的机器数表示为	00000000 00000000 00000000 00000101
// 取反之后表示为	11111111 11111111 11111111 11111010
// 该数是负数,负数补码与原码不同,先将其转为原码的反码:11111111 11111111 11111111 11111001
// 再转为原码:10000000 00000000 00000000 00000110
// 所以结果为:-6

<< :左移;各二进位全部左移若干位,高位丢弃(即左边超出的丢弃),低位补0(即右边差的)

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

图示:


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

图示:

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

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

图示:


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 个比特,结果也是非负的。)

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

图示:

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


位运算符的妙用

位运算判断奇数偶数

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

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

位运算实现两数交换

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

a = a + b;
b = a - b;
a = a - b;
// 或者
a = [b, (b = a)][0];
// 或者
[a, b] = [b, a]

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

// 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, 以此类推...

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

a = a^b
b = a^b
a = a^b

位运算实现乘除法

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

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

console.log(6 >> 1) // 输出3
console.log(6 << 1) // 输出12

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

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

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

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

  • 结果只会保留整数部分
console.log(7 >> 1) // 输出3

如果数值操作影响到了符号位,结果会错误

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

位元算实现取整

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

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

console.log(5.2 & -1) // 输出5
console.log(-7.2 & -1) // 输出-7

通过异或运算实现:

  • 将数值与0进行异或运算
console.log(5.2 ^ 0);// 输出5
console.log(-7.2 ^ 0);// 输出-7

将数值与另一个数进行两次异或运算

console.log(5.2 ^ 2 ^ 2); // 输出5
console.log(-7.2 ^ 2 ^ 2);// 输出-7

通过取反运算实现:将数值进行两次取反运算

console.log(~~5.2); // 输出5
console.log(~~-7.2); // 输出-7

通过左移或右移运算实现:将数值左移或右移0位

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

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

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

那么其对应状态如下


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

获取

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

// 举个例子,假设学生此时的状态码为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进行与运算,这样比较麻烦,所以上面的代码可以优化为

let result = v & ((0b1 << enums.camera.bitLength) - 1)

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

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]

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

const getProvide = (state, lowBitPos, bitLength) => {
   return (state >> lowBitPos) & bitArr[bitLength]
}

修改

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

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

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

封装为函数如下

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

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

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码