Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

js 二进制与位运算 #299

Open
aaaaaajie opened this issue Nov 25, 2019 · 0 comments
Open

js 二进制与位运算 #299

aaaaaajie opened this issue Nov 25, 2019 · 0 comments

Comments

@aaaaaajie
Copy link

js 二进制与位运算

生活中,我们经常用到的是以十进制为单位,进位是满 10 进 1,而计算机是用二进制,那么就是满 2 进 1 喽,为什么采用二进制呢?简单说下,他的由来其实是根据电路的开关闭合,电路呢只有 0 和 1,具体的详情这里不多做解释了,可以自行百科^_^。javascript 采用有符号 32 位的 2 进制,可表示 4294967295 个整数(含正负),范围是 -2147483648(2 的 32 次方) ~ 2147483647。

带符号二进制基本规则与几个概念

在计算机中规定最高位是符号位,0 为正,1 为负。正数表示是原码,负数是原码的补码。

原码

除去符号位,其他 32 位都为正。
例:00000000000000000000000000001010 原码 00000000000000000000000000001010。

反码

所谓反码,除符号位外,其他位 0 变 1,1 变 0。
例:1010 反码 1101

补码

所谓补码,就像是小学学过的 10 进制补数,举个例子就很好理解,3 的补数是多少?是 7。4 的补数是 6,补数就是 10 减去这个数。在二进制的补码就是相加等于 0,互为相反数。

  • 0 的补码
    0 的原码、反码、补码均为 0

  • 正数的补码

    原码

  • 负数的补码

    原码除符号位外的所有位取反+1。如何推出来的,看了一篇文章,挺有意思。点击查看

位运算

按位与(AND) &

描述:a & b 对于每一个比特位,只有两个操作数相应的比特位都是 1 时,结果才为 1,否则为 0。(摘自 MDN)

举例:2 & 3 = 2
    0000 0010
& 0000 0011
———————
    0000 0010

应用场景:

判断奇偶

console.log(num & 1) // 1 是奇数  0是偶数

权限设置:解权限(这个放到下面说)
因为奇数的最后一位永远是 1。
举例:num 是 3
0000 0011
0000 0001
—————
0000 0001

按位或(OR)|

描述:对于每一个比特位,当两个操作数相应的比特位至少有一个 1 时,结果为 1,否则为 0。(摘自 MDN)

举例:2 | 3 = 3
   0000 0010
 | 0000 0011
———————
    0000 0011

应用场景:

小数取整

function fn(n) {
  return n | 0
}
fn(3.2) // 3

权限
假设我们读的权限是 1 写的权限是 2,如果一个用户同时拥有读和写两种权限,正常来分配就是用一个数组[1,2],那么用或运算符如何分配呢?

class Permission {
  constructor() {
    this.instance = null
    this.permissions = {
      r: 1, // 读
      w: 2, // 写
      e: 4 // 执行
    }
  }
  getR() { return this.permissions.r }
  getW() { return this.permissions.w }
  getE() { return this.permissions.e }
  
  getRW() { return this.permissions.r | this.permissions.w }
  getRE() { return this.permissions.r | this.permissions.e }
  getWE() { return this.permissions.w | this.permissions.e }
  
  getRWE() { return this.permissions.r | this.permissions.w | this.permissions.e }
  getAll() { return Object.values(this.permissions).reduce((p1, p2, i) => p1 | p2) }
  
  isR(per) { return Boolean(per & this.permissions.r) }
  isW(per) { return Boolean(per & this.permissions.w) }
  isE(per) { return Boolean(per & this.permissions.e) }
  
  static getInstance() {
    !this.instance && (this.instance = new Permission())
    return this.instance
  }
}
const per = Permission.getInstance()
per.getRW() // 读写权限:3
per.getAll() // 所有权限:7
per.isR(3) // true 是否有读的权限
per.isW(5) // false 是否有写的权限
per.isE(5) // true 是否有执行的权限

注:基本运算过程
1 | 2 =>
  0000 0001
| 0000 0010
————————————
0000 0011
=> 3

按位异或(XOR)^

描述:对于每一个比特位,当两个操作数相应的比特位有且只有一个 1 时,结果为 1,否则为 0。(摘自 MDN)

举例:2 ^ 3
   0000 0010
^ 0000 0011
——————
    0000 0001

应用场景

变量交换
我们会用到两个变量的值进行交换,比如冒泡排序的两两对比,若前者比后者大就交换位置:

var a = 1,
  b = 2
var temp = a
var a = b
var b = temp

这里是声明第三个变量来实现交换。那么,用按位异或可以这么做:

var a = 1,
  b = 2
a ^= b
b ^= a
a ^= b

分解一下上面的:
a += 1 和 a ^= b 表达式规则是一样的
所以 a ^= b => a = a ^ b, 看下面运算过程:
    0000 0001
 ^ 0000 0010
——————
    0000 0011
此时,a 就变成了 3。
b ^= a => b = b ^ a
    0000 0010
 ^ 0000 0011
——————
    0000 0001
此时,b 变成了 1。
重复第一步:
    0000 0011
 ^ 0000 0001
——————
    0000 0010
此时,a 变成了 2。最终 a=2,b=1 实现了变量的交换。好处是少声明了一个临时变量,效率上,亲测结果 异或运算交换并不比第三变量的效率高(测试交换 100 次)。所以,这种方式待探讨,大家有答案的可以联系我,谢谢。

按位非(NOT)~

描述:反转操作数的比特位,即 0 变成 1,1 变成 0。
看到这个可能会想到上面所说的反码,这两者是有区别的,反码是按照有符号和无符号算的,取反是不管有没有符号全部 0 变 1,1 变 0 的

应用场景

小数取整
还有一个特征是取反是不能用于浮点型,所以可以用来向下取整,效果和 Math.floor()一样,~~效率要高。

function fn(n) {
  return ~~n
}
fn(3.2) // 3

左移(Left shift)<<

描述:a << b ,将 a 的二进制形式向左移 b (< 32) 比特位,右边用 0 填充。(摘自 MDN)
举例:3 << 4
0000 0011 向左移动 4 位 => 0011 0000
可知,左移数是越来越大的。移动运算符运算效率较高

应用场景

x 乘以 y 的 n 次方:x << y

1 << 3 => 0000 1000 化成10进制 => 1 * 2的3次方 => 8

2 进制化成 10 进制 就是 2 的 n 次方 乘以 当前位。

有符号右移 a >> b

描述:将 a 的二进制表示向右移 b (< 32) 位,丢弃被移出的位。
该操作符会将第一个操作数向右移动指定的位数。向右被移出的位被丢弃,拷贝最左侧的位以填充左侧。由于新的最左侧的位总是和以前相同,符号位没有被改变。所以被称作“符号传播”。
举例:
正数右移:4 >> 2

   0000 0000 0000 0000 0000 0000 0000 0100
>>                                       2
——————————————————————————————————————————
   0000 0000 0000 0000 0000 0000 0000 0001

正数右移,不够的位数用符号位 0 补全,可以发现规律,结果是 4 / (2 的 2 次方) => 当前位/(2 的移位的位数的次方)

负数右移:-3 >> 4

   1000 0000 0000 0000 0000 0000 0000 0011 的补码是=>
   1111 1111 1111 1111 1111 1111 1111 1101 向右移动4位,右边舍去,左侧用符号位补齐
>>                                       4
——————————————————————————————————————————
   1111 1111 1111 1111 1111 1111 1111 1111 补码是=>
   1000 0000 0000 0000 0000 0000 0000 0001 所以,最终是-1

无符号右移 a >>> b

描述:将 a 的二进制表示向右移 b (< 32) 位,丢弃被移出的位,并使用 0 在左侧填充。
正数右移:4 >>> 2

    0000 0000 0000 0000 0000 0000 0000 0100
>>>                                       2
———————————————————————————————————————————
    0000 0000 0000 0000 0000 0000 0000 0001

可以发现 4 >>> 2 和 4 >> 2 结果都是 1,也就是说,对于非负数来说,有无符号右移结果都是相同。
负数右移:-4 >>> 2

    1000 0000 0000 0000 0000 0000 0000 0100 补码=>
    1111 1111 1111 1111 1111 1111 1111 1100
>>>                                       2
———————————————————————————————————————————
    0011 1111 1111 1111 1111 1111 1111 1111
    化成10进制=> 2^29...2^1+1 = 1073741823

总结:

位运算 优势:乘除,取模,运算效率高 劣势:可读性较低,对初学者有难度,工作中应当按需所取,不应为炫技滥用。 好多源码都有用到位运算(express、koa、koa-body),现在学习了位运算,妈妈再也不担心读不懂源码了!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant