ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
[TOC] > 查看《编程基础》- 二进制基础 # JavaScript 位运算 JavaScript 将数字存储为 64 位浮点数,但**所有按位运算都以 32 位整型二进制数执行**。JavaScript 的浮点数遵循IEEE 754 规范。 在 ECMAScript 中,所有整数字面量默认都是有符号整数。 位运算只对整数起作用,如果一个运算子不是整数,会自动转为整数后再运行。虽然在 JavaScript 内部,数值都是以64位浮点数的形式储存,但是做位运算的时候,只能使用 31 位(0~2147483647),开发者是不能访问最高位的。 并且返回值也是一个32位带符号的整数。 下图展示的是数 18 的表示法: ![](https://img.kancloud.cn/fb/1d/fb1d2685120a42de410367780716467d_551x74.png) 1. 位运算只发生在整数上,因此一个非浮点数参与位运算之前会被向下取整,在控制台输入下面的代码: ~~~js 2.1 | 0 // 或运算 >>> 2 ~~~ 2. 为了避免访问符号位, javascript 在现实 负数的 二进制时,转换为 符号加上其绝对值的二进制,如: ~~~ (-123).toString(2); // "-1111011",即 - 和 123 的二进制 ~~~ JavaScript 中的这种整型是区分正负数的,根据上面的知识推断 js 中的整数的表示范围是: ~~~ -Math.pow(2,31) ~ Math.pow(2,31)-1 // [-2^31, +2^31-1] 即 [-2147483648, +2147483647] ~~~ 在控制台输出下面的代码来验证我们的推断。 ~~~js -2147483648 | 0 >>> -2147483648 -2147483649 | 0 >>> 2147483647 2147483647 | 0 >>> 2147483647 2147483648 | 0 >>> -2147483648 ~~~ 可以看出,如果一个超过 `2^31-1` 的整数参与位运算的时候就需要注意,其二进制溢出了,截取32位后,如果第 32 位是 1 将被解读为负数(补码)。 所以:大于和小于最低和最高的值再去进行转换时都将改变正负号。 ## 运算符 | 运算符 | 名称 | 描述 | | --- | --- | --- | | `&` | AND(与) | 两位都是 1 则设置每位为 1 | | `| `| OR(或) | 两位中有一个为 1 则设置每位为 1 | | `^` | XOR(异或) | 两位只有一位为 1 则设置每位为 1 | | `~` | NOT(非) | 反转所有位 | | `<<` | 左移 | 通过从右填充 0 向左位移,并使最左边的位移出 | | `>>` | 有符号右位移 | 通过从左填充最左位的拷贝来向右位移,并使最右边的位移出 | | `>>>` | 无符号右移 | 通过从左填充 0 来向右位移,并使最右边的位移出 | 由于 JavaScript 使用 32 位有符号整数,`~ 5` 将返回 `-6`。 ```js 00000000000000000000000000000101 // 5 11111111111111111111111111111011 // -5 (负数是正数的二进制补码加 1) 11111111111111111111111111111010 // ~5 = -6 ``` 1. 在执行位运算之前,JavaScript 将数字转换为 32 位有符号整数。 2. 执行按位操作后,结果将转换回 64 位 JavaScript 数。 ## 演示 > 使用 4 位无符号二进制数进行演示 | 操作 | 结果 | 等同于 | 结果 | | --- | --- | --- | --- | | `5 & 1` | 1 | 0101 & 0001 | 0001 | | `5 | 1` | 5 | 0101 | 0001 | 0101 | | `5 ^ 1` | 4 | 0101 ^ 0001 | 0100 | | `~ 5` | 10 | ~0101 | 1010 | | `5 << 1` | 10 | 0101 << 1 | 1010 | | `5 >> 1` | 2 | 0101 >> 1 | 0010 | | `5 >>> 1` | 2 | 0101 >>> 1 | 0010 | 上面的例子使用 4 位无符号二进制数。所以 `~ 5` 返回 10。 下面举例子来说明每个运算符的作用,开始之前先来介绍几个会用到的知识点 # 原生二进制字面量 ES6 中引入了原生二进制字面量,二进制数的语法是`0b`开头,我们将会用到这个新功能,chrome 最新版已经支持。 ~~~ 0b111 // 7 0b001 // 1 (0b1100).toString(10) // 0b1100 === 12 ~~~ js 中二进制和十进制如何转换呢? ~~~ // 十进制 => 二进制 let num = 10; console.log(num.toString(2)); // 二进制 => 十进制 let num1 = 1001; console.log(parseInt(num1, 2)); ~~~ ## `Number.prototype.toString` 先来介绍下下面会用到的一个方法——`Number.prototype.toString`方法可以讲数字转化为字符串,有一个可选的参数,用来决定将数字显示为指定的进制,下面可以查看3的二进制表示 ~~~ 3..toString(2) >> 11 ~~~ ## `& 与` &按位与会将操作数和被操作数的相同为进行与运算,如果都为1则为1,如果有一个为0则为0 ~~~ 101 011 --- 001 ~~~ 101 和 011与完的结果就是 001,下面在js中进行验证 ~~~ (0b101 & 0b011).toString(2) >>> "1" ~~~ ## `| 或` |按位或是相同的位置上只要有一个为1就是1,两个都为0则为0 ~~~ 101 001 --- 101 ~~~ 101 和 001或完的结果是101,下面在js中进行验证 ~~~ (0b101 | 0b001).toString(2) >>> "101" ~~~ ## `~ 非` `~`操作符会将操作数的每一位取反,如果是1则变为0,如果是0则边为1 ~~~js 101 --- 010 ~~~ 101按位非的结果是010,下面在js中验证 ~~~js (~0b101).toString(2) >>> "-110" ~~~ 啊呀,怎么结果不对呢!!!上面提到了 js 中的数字是有符号的,我们忘记了最高位的符号了,为了简化我们将32位简化为8位,注意最高位是符号位 ``` > 0 0000101 > > 1 1111010 // 求非 > > 1 0000101 // 求反 > > 1 0000110 // 求补 ``` `1 1111010`明显是一个负数,而且是负数的补码表示,我们的求它的原码,也就是再对它求补`1 0000110`就是这个数的真值,也就是结果显示`-110`,这下总算自圆其说了,O(∩_∩)O 哈哈~ 其实上面的与和或也都是会操作符号位的,不信你试试下面这两个,可以看到符号位都参与了运算 ~~~js (0b1&-0b1) >>> 1 (0b1|-0b1) >>> -1 ~~~ ## `^ 异或` 参与运算的两个值,如果两个相应 bit 位相同,结果为0,否则为1: 按位异或的3个特点: 1) `0^0=0,0^1=1` 0异或任何数=任何数 2) `1^0=1,1^1=0` 1异或任何数-任何数取反 3) 任何数 异或 自己 = 把自己置 0 ~~~js 101 001 --- 100 ~~~ 101 和 001异或的结果是 100,js中验证 ~~~js (0b101^0b001).toString(2) >>> "100" ~~~ ### 常见用途 1. 判断两个整数a,b是否相等,则可通过下列语句实现: ``` (a ^ b) === 0 ``` 2. 使某些特定的位翻转 例如:对数10100001的第2位和第3位翻转,则可以将该数与00000110进行按位异或运算。 ``` 10100001 ^ 00000110 = 10100111 ``` 3. 实现两个值的交换,而不必使用临时变量。 例如交换两个整数 a=10100001,b=00000110 的值,可通过下列语句实现: ``` a = a ^ b;   // a=10100111 b = b ^ a;   // b=10100001 a = a ^ b;   // a=00000110 ``` 4. 记录多个信息 利用位的异或运算使用一个数字记录多个信息: 有几个状态值分别是 1、2、4、8、16 ..... | 二进制表示 | 十进制值 | | --- | --- | | 00000000000000000000000000000001 | 1 | | 00000000000000000000000000000010 | 2 | | 00000000000000000000000000000100 | 4 | | 00000000000000000000000000001000 | 8 | | 00000000000000000000000000010000 | 16 | | 00000000000000000000000000100000 | 32 | | 00000000000000000000000001000000 | 64 | 这些值的规律是,他们的二进制只有一位是 1 ,其余都是 0, 因此知道它们任意几个的按位异或运算的结果,就知道是哪几个数的组合,这样可以用一个数字记录多个信息。 ```js 1^2^4 = 7  // "00000111" ``` 因此,如果我们知道结果是 7 ,就知道它们是由 1 、2、4 组合而成。 5. 被用在一些加密算法中 ## `<< 左移` 左移的规则将操作数向左移动指定的位数,右侧用 0 补充,其效果相当于 ×2,其实计算机就是用移位操作来计算乘法的 ~~~js 010 --- 0100 ~~~ 010左移一位就会变为100,下面在js中验证 ~~~ (0b010<<1).toString(2) >>> "100" ~~~ ## `>> 有符号右移` 该操作符会将第一个操作数向右移动指定的位数。向右被移出的位被丢弃,拷贝最左侧的位以填充左侧。由于新的最左侧的位总是和以前相同,符号位没有被改变。所以被称作“符号传播”。 > 对任一数值 x 进行右移n, 相当于十进制里的除以10的倍数,在这里是指除以数之后取整。 > ~~~js > x / 2^n > ~~~ ~~~js (0b111>>1).toString(2) >>> "11" (-0b111>>1).toString(2) >>> "-100" ~~~ 负数的结果好像不太对劲,我们来看看是怎么回事 ``` > -111 // 真值 > 1 0000111 // 原码 > 1 1111001 // 补码 > 1 1111100 // 算数右移 > 1 0000100 // 移位后的原码 > -100 // 移位后的真值 ``` ## `>>> 无符号右移` 该操作符会将第一个操作数向右移动指定的位数。向右被移出的位被丢弃,左侧用0 填充。因为符号位变成了 0,所以结果总是非负的。对于整数和 `>>` 没有区别。(译注:即便右移 0 个比特,结果也是非负的。) ~~~ (0b111>>>1).toString(2) >>> "11" ~~~ 对于负数则就不同了,右移后会变为正数 ~~~ (-0b111>>>1).toString(2) >>> "1111111111111111111111111111100" ~~~ ## 要注意的地方 如果运算元不是可用的整数,将取 0 作为运算元: ```js ~NaN; // 将执行 ~0 ,结果为 -1 ~'x'; // -1 'hello'|0; // 0 ({})|0 ; //0 ~Infinity; //-1 同 ~0 ``` 位移运算不能移动超过31位,如果试图移动超过31位,将 位数 对32取模后再移位 ```js 123 >> 32 //实际是 123>>0 (32%32 = 0) 123 >> 33 //实际是 123>>1 ``` # 关于开头的问题 关于二进制数就说这么多吧,再来说说开头的问题,开头的问题其实可以分解为下面的问题因为search会返回-1 和找到位置的索引,也就成了下面的问题 ~~~ !~-1 >>> ture !~0 >>> false !~1 >>> false ~~~ 非运算对于数字的结果相当于改变符号,并对其值的绝对值-1 ~~~ ~-1 >>> 0 ~0 >>> -1 ~1 >>> -2 ~~~ 其实可以看出!~x的逻辑就是判断x是否为-1,my god这逻辑真是逆天了,我还是劝大家直接写成 x === -1多好啊> > [聊聊JavaScript中的二进制数](https://yanhaijing.com/javascript/2016/07/20/binary-in-js/) # 参考 [w3school -JavaScript 位运算符](https://www.w3school.com.cn/js/js_bitwise.asp) [Javascript 中的二进制运算](https://www.cnblogs.com/ecalf/archive/2012/11/26/2789870.html)