D2 - 先生 帮您带位元运算子

前言

JavaScript 运算子大概可以分为几大类:算数运算子、逻辑运算子、赋值运算子、比较运算子和位元运算子...

大部分的运算子在阅读规范後都能快速上手,但其中的位元运算子在理解上相对没那麽直觉,但 只要掌握了二进制表达方式,这就只是块小小小鸡蛋糕了

所以 先从了解 二进制binary 开始吧! /images/emoticon/emoticon42.gif

二进制表达方式

在数字系统里,以进位制表达的方式有 2、8、10、16 进位等,而一般日常熟悉的数字都是十进制,也就是以 10 作为进位基数的表达方式。

二进制 就是以 2 为底数的记数方式,以10 做数字表示,目前计算机就是以二进制的方式来储存资料,1 代表open 0代表 off, 而每个数字就称为一个 位元bit

下图是二进制与十进制数字的表示比较

JS 中的位元运算子有哪些?

这次要介绍 6 种位元运算子 bitwise operator :

& Bitwise AND
| Bitwise OR
~ Bitwise NOT
^ Bitwise XOR
<< Left Shift
>> Sign-Propagating Right Shift

And & 位元运算子

是不是很眼熟? 这个符号 & 在逻辑运算子中也有!
逻辑运算子中的 And && 代表两个条件都符合才是 true, 其一不符合就是 false

true  && true  === true
false && false === false
false && true  === false

而位元运算子的 & 也是同样逻辑, 先将数字转换为二进制後,把握 0 代表 false, 1 代表 true 的原则,再来就是一样的逻辑判断。

所以当 1 和 0 比对,等同比较 true && false,结果是 false 0
那 1 和 1 呢? true && true 比对的结果当然就是 true 1

小试身手 >> 3 & 2 结果是多少?

//先将 3 和 2 转为二进制表达
//每个位元做 1 , 0 比较

   011  -> 3
&  010  -> 2
--------
   010  -> 2
   
 第一位: 1 & 0 === 0
 第二位: 1 & 1 === 1
 第三位: 0 & 0 === 0

再来一题 >> 5 & 7

   101  -> 5
&  111  -> 7
--------
   101  -> 5

判断奇数偶数

由上面的举例发现, 在二进制表示法中,奇数的第一位数都是 1,偶数的第一位都是 0,利用此特性就可以轻松筛选出奇数偶数

//重点比对第一位, 所以配上 & 1 判断第一位是否为 1 或 0

10 & 1 ->0000
9 & 1 -> 0001
8 & 1 -> 0000
7 & 1 -> 001
6 & 1 -> 000
5 & 1 -> 001
4 & 1 -> 000
3 & 1 -> 001
2 & 1 -> 000
1 & 1 -> 001

//奇数 & 1 结果都是 1
//偶数 & 1 结果都是 0
//实作判断奇数偶数 function

function isOdd(number){
    return (number & 1)===1;
}

function isEven(number){
    return (number & 1)===0;
}

isOdd(3) === true
isEven(6) === true

OR | 位元运算子

| 运算子只是将比对规则改为 OR 判断,有 1 就 return 1 否则 return 0

1 true  || 1 true  === 1 true
0 false || 0 false === 0 false
0 false || 1 true  === 1 true

刚刚 3 & 2 === 2 改为 3 | 2 ?

  011  -> 3
| 010  -> 2
------
  011 -> 3
  
 第一位: 1 | 0 === 1
 第二位: 1 | 1 === 1
 第三位: 0 | 0 === 0
段落重点:
  • & 运算子: 1 & 1 return 1, 而0 & 1 return 0
  • | 运算子: 1 | 1 return 1, 而0 | 1 return 1

XOR ^ 位元运算子

^ XOR 运算子,全名 Exclusive Or,与一般的逻辑不同,^ 是当数字两两相同是 false 0 ,不同则是true 1

一样用 3 ^ 2 试看看吧

  011  -> 3
^ 010  -> 2
------
  001  -> 1
  
 第一位: 1 ^ 0 === 1
 第二位: 1 ^ 1 === 0
 第三位: 0 ^ 0 === 0

<< Left Shift Operator 和 >> Right Shift Operator

这两个运算子用来移动数字往符号指向的方向,<< 代表位元向左移, 空出来的位元补上0>> 代表位元向右移。

NOT ~ 位元运算子

~ NOT 运算子使用的是 补数 implement 的概念,数字被转换为 32 位元的二进制表达,将 0 转为11 转为 0

修但几咧 补数是啥东东

看到这个名词时 google 看了很多文章,wiki 的解释点了不下 10 次

正数和 0 的二补数就是该数字本身,负数的二补数则是将其对应正数按位元取反再加 1 - wikipedia

一直无法透彻理解为什麽要这样设计,直到我看到这个 youtube 影片

Twos complement: Negative numbers in binary 推推! 原来是这样啊

你有想过以二进制怎麽表示负数吗?

实际上电脑会分配一个符号位 sign bit,来作为正负号表示, 0 表示正数 1表示负数

第一种最直觉的负数表示为 原码 sign-and-magnitude
但使用这种表达方式会发现一个问题
电脑进行 -5 + 5 的运算时,结果会是 1101 + 0101 = 10010 等於 2 ?
安堆~~~~

//原码 sign-and-magnitude

sign
 bit  4  2  1
 --------------
  1   1  1  1  // -7
  1   1  1  0  // -6
  1   1  0  1  // -5
  1   1  0  0  // -4
  1   0  1  1  // -3
  1   0  1  0  // -2
  1   0  0  1  // -1
  1   0  0  0  // -0
  0   0  0  0  //  0
  0   0  0  1  //  1
  0   0  1  0  //  2
  0   0  1  1  //  3
  0   1  0  0  //  4
  0   1  0  1  //  5
  0   1  1  0  //  6
  0   1  1  1  //  7

看来上面那种负数表示方法不太适用於电脑运算
那有另一种表示方法称为 一补数(ones' complement)

在一补数表示时要做位元转换,负数是将 1 0 互换,正数不变动。
所以 -5 是从 0101 位元转换为 1010 表示

那这种方法有解决加总的结果吗?

再试试 -5 + 5 , 1010 + 0101 = 10000 等於 -0
恩 看来好一点,但这个负号不觉得怪怪的吗 -0

再另外试试 5 + (-3)
0101 + 1100 = 100001 因为只取4位数先不看最左边的 1,加总起来的数字变为 1
不行啊 数字还是不对

//一补数 ones' complement

sign
 bit  4  2  1
 --------------
  1   0  0  0  // -7
  1   0  0  1  // -6
  1   0  1  0  // -5
  1   0  1  1  // -4
  1   1  0  0  // -3
  1   1  0  1  // -2
  1   1  1  0  // -1
  1   0  0  0  // -0
  0   0  0  0  //  0
  0   0  0  1  //  1
  0   0  1  0  //  2
  0   0  1  1  //  3
  0   1  0  0  //  4
  0   1  0  1  //  5
  0   1  1  0  //  6
  0   1  1  1  //  7

在一补数的正负数加总中可以发现和正确的答案都只差 1
所以 就产生了 一补数加上 1 的方式 二补数(two's complement)

再试一次刚刚的加总 -5 + 5
1011 + 0101 = 10000 一样不看最左边的数字,很好 等於0

5 + (-3) 呢?
0101 + 1101 = 10010
耶 是我们要的 2

//二补数 two's complement

sign
 bit  4  2  1
 --------------
  1   0  0  1  // -7 
  1   0  1  0  // -6
  1   0  1  1  // -5
  1   1  0  0  // -4
  1   1  0  1  // -3
  1   1  1  0  // -2
  1   1  1  1  // -1
  0   0  0  0  //  0
  0   0  0  1  //  1
  0   0  1  0  //  2
  0   0  1  1  //  3
  0   1  0  0  //  4
  0   1  0  1  //  5
  0   1  1  0  //  6
  0   1  1  1  //  7

其实二补数的负数算法也很简单,只要将最左边的数字带上负号做加总就跟 原码 sign-and-magnitude 算法一样

1001 -> -8+1 = -7
1010 -> -8+2 = -6

二补数很好的解决了加总的问题,也不存在奇怪的 -0
现在再看看 wiki 说的

正数和 0 的二补数就是该数字本身。负数的二补数则是将其对应正数按位元取反再加 1

是不是就理解了呢?

透过补数进行位元 NOT 运算

看完上面了解补数的运作後,~ NOT 就是对每个位元进行补数转换

 sign
 bit  4  2  1                ~ NOT 
 --------------------------------------- 
  1   0  0  1  // -7  | ~-7  0110  // 6
  1   0  1  0  // -6  | ~-6  0101  // 5
  1   0  1  1  // -5  | ~-5  0100  // 4
  1   1  0  0  // -4  | ~-4  0011  // 3
  1   1  0  1  // -3  | ~-3  0010  // 2
  1   1  1  0  // -2  | ~-2  0001  // 1
  1   1  1  1  // -1  | ~-1  0000  // 0
  0   0  0  0  //  0  | ~0   1111  // -1
  0   0  0  1  //  1  | ~1   1110  // -2
  0   0  1  0  //  2  | ~2   1101  // -3
  0   0  1  1  //  3  | ~3   1100  // -4
  0   1  0  0  //  4  | ~4   1011  // -5
  0   1  0  1  //  5  | ~5   1010  // -6
  0   1  1  0  //  6  | ~6   1001  // -7
  0   1  1  1  //  7  | ~7   1000  // -8

如果要在脑中做补位转来转去太困难,其实也可以直接套用这个公式 -(x + 1),就可以知道 ~下的数字是多少了!
做做下面几题练练看吧

~16
~56
~63

 sign
 bit  
 64  32  16  8  4  2  1              
 ------------------------------------ 
  0   0   1  0  0  0  0   // 16
  1   1   0  1  1  1  1   // ~16 -> -64+32+8+4+2+1 = -17
  0   1   1  1  0  0  0   // 56
  1   0   0  0  1  1  1   // ~56 -> -54+4+2+1 = -57
  0   1   1  1  1  1  1   // 63
  1   0   0  0  0  0  0   // ~63 -> -64

double tilde ~~

这里教个小技巧,你知道在JS中 ~~ 3.2 是什麽意思吗
刚刚学到 ~ 是补位,那两个 ~~ 是补位再补位,等於自己本身,那 这个有什麽意义吗?

适用在对非整数数字无条件舍去! 因为 JavaScript 位元运算子中都是将数字转为 32 位元的整数呈现,所以可以利用这个原则得到取整数这个效果

//对小数点 2.5 进行两次 NOT 位元运算

~2.5 === -3
~(-3)=== 2

以上
一直很想好好整理的位元运算子完成了呢 /images/emoticon/emoticon42.gif
其中补数观念还不太理解的朋友真的推荐去看看这支影片 Twos complement: Negative numbers in binary

对了 在 Mac 的计算机也可以进行位元运算唷~~~
在计算机上按快捷键 cmd + 3 就能快速切换成程序设计计算机
来玩玩看吧!


<<:  LiteX/VexRiscv 简介与使用 (一) 太初有光

>>:  Graph-Tree: uva 615 Is It A Tree?

[Day - 23] - Spring Reactor之进入忍者龟的Flux

Abstract 卡哇勾嘎!!!想必大家都有开发过Publisher-Subscriber架构,有些...

Day 13: Structural patterns - Composite

目的 将程序的组成转换成有上下阶级的结构(或称:树状结构),方便使用者不论从哪个节点、叶子使用,都可...

[Day10] 团队管理:人才定位与任务给予

掌握人才状况 以表现与潜能为人才定位 我们审慎面试人进来後,对於如何有效沟通与解决问题的mental...

Day21 - Sort

大家好我是长风青云。今天是铁人赛21天。我们算是半只脚踏入演算法的阶段。(因为Sort的部分DS和A...

Android学习笔记21

接下来要实作跳转之後的activty连接着viewpager跟tabitem去对应到相对的fragm...