文章已同步至掘金:https://juejin.cn/post/7100867308867813389
欢迎访问😃,有任何问题都可留言评论哦~
前言 {#%E5%89%8D%E8%A8%80}
前段时间无意间在项目中看到了这么一行代码
let x = m + n >> 1
虽然当时知道这是位运算符,但是对他的原理以及运算的结果还是有点不太清楚,遂搜集资料,整理了这篇文章
其实上面代码可以转化为:
let x = Math.floor((m + n) / 2)
逻辑运算符 {#%E9%80%BB%E8%BE%91%E8%BF%90%E7%AE%97%E7%AC%A6}
| 运算符 | 描述 | |------|-----| | && | and | | || | or | | ! | not |
位运算符 {#%E4%BD%8D%E8%BF%90%E7%AE%97%E7%AC%A6}
| 运算符 | 名称 | 描述 | |--------|--------|------------------------------| | & | AND | 如果两位都是 1 则设置每位为 1 | | | | OR | 如果两位之一为 1 则设置每位为 1 | | ^ | XOR | 如果两位只有一位为 1 则设置每位为 1 | | ~ | NOT | 反转所有位 | | << | 零填充左位移 | 通过从右推入零向左位移,并使最左边的位脱落。 | | >> | 有符号右位移 | 通过从左推入最左位的拷贝来向右位移,并使最右边的位脱落。 | | >>> | 零填充右位移 | 通过从左推入零来向右位移,并使最右边的位脱落。 |
特点 {#%E7%89%B9%E7%82%B9}
- 在Javascript中使用位运算符,底层会先将数字转为整数(因为JS中所有的数字都保存为双精度浮点数),然后再进行计算(比如经常利用位运算符取整)
- 位运算直接对二进制位进行计算(即按照内存中表示数值的位来操作数值),直接处理每一个比特位
- 位操作符并不直接操作64位的值。而是先将64位的值转换为32位的整数,再执行操作,最后再将结果转换为64位
对于有符号的整数,32位中的后31位用于表示整数的值,第1位表示数值的符号:0表示正数,1表示负数。
假如有一个值为5
,5
转换成二进制为 101
,转换过程:
但是因为它占用4字节(32位),所以前面填了一堆0,即 00000000 00000000 00000000 00000101
那么 -5
呢?
在计算机中,负数是以其正值的补码形式来表达的,如下
原码:一个整数,按照绝对值大小转换成的二进制数,称为原码
比如:5 的原码为 00000000 00000000 00000000 00000101
反码:将二进制数按位取反,所得的新二进制数称为原二进制数的反码(1变0,0变1)
比如:5 的反码为 11111111 11111111 11111111 11111010
`补码:反码加 1 称为补码
比如:5 的补码为 11111111 11111111 11111111 11111011
`
结论:
正数
正数以纯二进制格式存储,31位中的每1位都表示2的幂(还有1位是符号位)。第一位表示2º,第二位表示2¹,第三位表示2²,以此类推。没有用到的位以0表示(即忽略不计)。
负数
负数同样以二进制码存储,但使用的格式是二进制补码。计算一个数值的二进制补码(即求一个负数的二进制码),需要经过下列3个步骤:
- 求这个数值绝对值的二进制码
- 求二进制反码,即将0替换为1,将1替换为0
- 得到的二进制反码+1
原理 {#%E5%8E%9F%E7%90%86}
按位 与(AND):& {#%E6%8C%89%E4%BD%8D-%E4%B8%8E%EF%BC%88and)%EF%BC%9A%26}
& 以 特定的方式 组合操作 二进制 数中对应的位, 如果对应的位都为1,那么结果就是1。如果任意一个位是0,则结果就是0
1 的二进制:00000000 00000000 00000000 00000001
2 的二进制:00000000 00000000 00000000 00000010
1 \& 2 最终结果(也就是0):00000000 00000000 00000000 00000000
1 的二进制:00000000 00000000 00000000 00000001
3 的二进制:00000000 00000000 00000000 00000011
`1 & 3 最终结果(也就是1):00000000 00000000 00000000 00000001
`
按位 或(OR):| {#%E6%8C%89%E4%BD%8D-%E6%88%96%EF%BC%88or)%EF%BC%9A%7C}
| 运算符跟 & 的区别在于如果对应的位中任一个操作数为1,那么结果就是1
1 的二进制:00000000 00000000 00000000 00000001
2 的二进制:00000000 00000000 00000000 00000010
1 \| 2 最终结果(也就是3):00000000 00000000 00000000 00000011
1 的二进制:00000000 00000000 00000000 00000001
3 的二进制:00000000 00000000 00000000 00000011
`1 | 3 最终结果(也就是3):00000000 00000000 00000000 00000011
`
按位 异或(XOR):^ {#%E6%8C%89%E4%BD%8D-%E5%BC%82%E6%88%96%EF%BC%88xor)%EF%BC%9A%5E}
^ 如果对应两个操作位 有且仅有1个1时结果为1,其它都是0
1 的二进制:00000000 00000000 00000000 00000001
2 的二进制:00000000 00000000 00000000 00000010
1 \^ 2 最终结果(也就是3):00000000 00000000 00000000 00000011
1 的二进制:00000000 00000000 00000000 00000001
3 的二进制:00000000 00000000 00000000 00000011
`1 ^ 3 最终结果(也就是2):00000000 00000000 00000000 00000010
`
按位 非(NOT):~ {#%E6%8C%89%E4%BD%8D-%E9%9D%9E%EF%BC%88not)%EF%BC%9A~}
~ 运算符是对位求反,1变0,0变1,也就是求二进制的反码
2 的二进制:00000000 00000000 00000000 00000010
反码:11111111 11111111 11111111 11111101
由于第一位(符号位)是1,所以这个数是一个负数。
JavaScript 内部采用补码形式表示负数,即需要将这个数减去1,再取一次反,然后加上负号,才能得到这个负数对应的10进制值。
如下:
反码 -1
11111111 11111111 11111111 11111100
取反
00000000 00000000 00000000 00000011
加负号
\~2 的最终结果:-3
3 的二进制:00000000 00000000 00000000 00000011
反码:11111111 11111111 11111111 11111100
反码 -1
11111111 11111111 11111111 11111011
取反
00000000 00000000 00000000 00000100
`加负号
~3 的最终结果:-4
`
左移(Left shift):<< {#%E5%B7%A6%E7%A7%BB%EF%BC%88left-shift)%EF%BC%9A%3C%3C}
<< 运算符使指定的二进制数所有位都左移指定次数,其移动规则为:丢弃高位,低位补0(即按照二进制形式把所有的数字向左移动对应的位数,高位移除(舍弃),低位的空位补0)
1 的二进制:00000000 00000000 00000000 00000001
1 \<\< 2 (即左移2位)
00000000 00000000 00000000 00000001
00000000 00000000 00000000 0000000100
抛弃向左移动的两个0,后面补0,结果二进制如下
00000000 00000000 00000000 00000100
1 \<\< 2 的最终结果:4
1 \<\< 3 (即左移3位)
00000000 00000000 00000000 00000001
00000000 00000000 00000000 00000001000
`抛弃向左移动的三个0,后面补0,结果二进制如下
00000000 00000000 00000000 00001000
1 << 3 的最终结果:8
`
结论:
M << N,相当于:M * (2**N)
有符号右移:>> (又称 "符号传播") {#%E6%9C%89%E7%AC%A6%E5%8F%B7%E5%8F%B3%E7%A7%BB%EF%BC%9A%3E%3E-%EF%BC%88%E5%8F%88%E7%A7%B0-%E2%80%9C%E7%AC%A6%E5%8F%B7%E4%BC%A0%E6%92%AD%E2%80%9D%EF%BC%89}
>> 该操作符会将指定操作数的二进制位向右移动指定的位数。向右被移出的位被丢弃,拷贝最左侧的位以填充左侧(这里是和无符号右移的最大区别)。由于新的最左侧的位总是和以前相同,而符号位并没有被改变。所以被称作"符号传播"
3 >> 2
3 的二进制:00000000 00000000 00000000 00000011
00000000 00000000 00000000 00000011
00000000 00000000 00000000 00000011
拷贝最前面两位00,抛弃向右移除的两位11,结果二进制如下
00000000 00000000 00000000 00000000
3 \>\> 2 最终结果:0
4 \>\> 1
4 的二进制:00000000 00000000 00000000 00000100
`00000000 00000000 00000000 00000100
00000000 00000000 00000000 00000100
拷贝最前面一位0,抛弃向右移除的一位0,结果二进制如下
000000000 00000000 00000000 0000010
4 >> 1 最终结果:2
`
无符号右移:>>> {#%E6%97%A0%E7%AC%A6%E5%8F%B7%E5%8F%B3%E7%A7%BB%EF%BC%9A%3E%3E%3E}
该操作符会将第一个操作数向右移动指定的位数。向右被移出的位被丢弃,左侧用0填充(这点是和有符号右移最大的区别)。因为符号位变成了0,所以结果总是非负的。
注:对于非负数,有符号右移和无符号右移总是返回相同的结果。
3 >>> 2
3 的二进制:00000000 00000000 00000000 00000011
00000000 00000000 00000000 00000011
00000000 00000000 00000000 00000011
拷贝最前面两位00,抛弃向右移除的两位11,结果二进制如下
00000000 00000000 00000000 00000000
3 \>\>\> 2 最终结果:0
4 \>\>\> 2
4 的二进制:00000000 00000000 00000000 00000100
`00000000 00000000 00000000 00000100
00000000 00000000 00000000 00000100
拷贝最前面一位0,抛弃向右移除的一位0,结果二进制如下
000000000 00000000 00000000 0000001
4 >>> 2 最终结果:1
`
应用 {#%E5%BA%94%E7%94%A8}
判断奇偶
// 偶数 & 1 = 0
// 奇数 & 1 = 1
console.log(2 & 1) // 0
console.log(3 & 1) // 1
取整
console.log(~~ 1.1) // 6
console.log(1.1 >> 0) // 6
console.log(1.1 << 0) // 6
console.log(1.1 | 0) // 6
// >>>不可对负数取整
console.log(1.1 >>> 0) // 6
除以2向下取整
console.log(4 >> 1);
console.log(5 >> 1);
值交换
let m = 1
let n = 2
m ^= n
n ^= m
m ^= n
console.log(m) // 2
console.log(n) // 1
判断正负
console.log(1 === 1 >>> 0)
console.log(-1 === -1 >>> 0)