简介:
我们在JS的书写过程中通常都是使用十进制运算,但是也是会有一些少数的场景需要使用到二进制运算,比如在Vue3的源码中就通过使用二进制运算来实现对effect函数的深度进行记录以及比较,来优化响应式以及防止无限循环等等这是我搜集并总结的一些常用二进制运算方法
位运算符有 7 个,分为两类:
逻辑位运算符:位与(&)、位或(|)、位异或(^)、非位(~)
移位运算符:左移(<<)、右移(>>)、无符号右移(>>>)
逻辑位运算符
1.&
“&”运算符(位与)用于对两个二进制操作数逐位进行比较,比较规则如下。
第一个数字的位置 | 第二个数字的位置 | 第二个数字的位置 |
---|---|---|
1 | 1 | 1 |
1 | 0 | 0 |
0 | 1 | 0 |
0 | 0 | 0 |
其实就是执行我们常用的&&比较方法一样,1代表true,0代表false,
1 && 1 = true // 1
0 && 1 = false // 0
1 && 0 = false // 0
0 && 0 = false // 0
3 & 9 // 等于1
0000 0000 0000 0000 0000 0000 0000 0011 // 3
0000 0000 0000 0000 0000 0000 0000 1001 // 9
--------------------------------------------
0000 0000 0000 0000 0000 0000 0000 0001 // 1
总结起来就是全部是1结果是1其他都是0
2.|
“|”运算符(位或)用于对两个二进制操作数逐位进行比较,比较规则如下。
第一个数字的位置 | 第二个数字的位置 | 第二个数字的位置 |
---|---|---|
1 | 1 | 1 |
1 | 0 | 1 |
0 | 1 | 1 |
0 | 0 | 0 |
这与我们比较运算中的||一样,有一个true就是true,都不是true才是false
1 || 1 = true // 1
0 || 1 = true // 1
1 || 0 = true // 1
0 || 0 = false // 0
3 | 9 // 等于11
0000 0000 0000 0000 0000 0000 0000 0011 // 3
0000 0000 0000 0000 0000 0000 0000 1001 // 9
--------------------------------------------
0000 0000 0000 0000 0000 0000 0000 1011 // 11
应用:
数字类型取整数,运算更加高效,因为其生成的32位是整数存储,所以会直接舍弃小数部分的数值,我们只需和0进行比较就可以返回原来的数值的整数部分,但是其最高只有存储32位,但是我们js内最高存储位数是53位所以使用的时候需要确定数字的最大位数才行
10.2341 | 0 // 10
3.^
“^”运算符(位异或)用于对两个二进制操作数逐位进行比较,比较规则如下。
第一个数字的位置 | 第二个数字的位置 | 第二个数字的位置 |
---|---|---|
1 | 1 | 0 |
1 | 0 | 1 |
0 | 1 | 1 |
0 | 0 | 0 |
3 ^ 9 // 等于10
0000 0000 0000 0000 0000 0000 0000 0011 // 3
0000 0000 0000 0000 0000 0000 0000 1001 // 9
--------------------------------------------
0000 0000 0000 0000 0000 0000 0000 1010 // 10
总结:就是前后两个一样就是0,不一样就是1
4.~
“~”运算符(位非)用于对一个二进制操作数逐位进行取反操作。
第 1 步:把运算数转换为 32 位的二进制整数。
第 2 步:逐位进行取反操作。
第 3 步:把二进制反码转换为十进制浮点数。
注意的是这边的取反和我们熟悉的二进制反码不一样,反码取反是符号位不变,其他位取反,而~的取反是所有位都取反
这边我们先来温习一下反码
**特点:**正数的原码,反码,补码都是一样的,我们说的不一样主要是在负数上,因为负数在我们计算机内存储的是补码的形式,下面我讲解一下负数的反码,补码:
原码:一个数正常负数应该存粗的二进制情况
反码:将原码除了符号位其他全部取反
补码:将反码再加一
-10
1000 0000 0000 0000 0000 0000 0000 1010 // 原码
1111 1111 1111 1111 1111 1111 1111 0101 // 反码 = 原码除符号位取反
1111 1111 1111 1111 1111 1111 1111 0110 // 补码 = 反码+1
也就是对于计算机来说1111 1111 1111 1111 1111 1111 1111 0110才是代表-10,而不是我们理解的原码,之所以这样存储是为了方便计算机的计算,因为如果那正数的原码和负数的原码进行相加会出错可以看到下面:
1000 0000 0000 0000 0000 0000 0000 1010 // -10原码
0000 0000 0000 0000 0000 0000 0000 1001 // 9原码
--------------------------------------------- 相加
1000 0000 0000 0000 0000 0000 0001 0011 // -19 显然是不对的
正确的:
1111 1111 1111 1111 1111 1111 1111 0110 // -10补码
0000 0000 0000 0000 0000 0000 0000 1001 // 9补码
--------------------------------------------- 相加
1111 1111 1111 1111 1111 1111 1111 1111 // 得到
// 因为符号为是1所以这还是一个负数,但是现在是补码,我们要得到其10进制的值需要将其转化为原码
1111 1111 1111 1111 1111 1111 1111 1110 // 减1得到反码
1000 0000 0000 0000 0000 0000 0000 0001 // 取反得到-1
好了了解了补码,然后我们回到我们 ~ 符号的运算逻辑上来:
~9 // 等于-10
0000 0000 0000 0000 0000 0000 0000 1001 // 9 这是原码
// 进行~运算全部取反可以得到如下,发现符号位是1也就是负数,这在计算机中是这么存储的,但是我们不知道这代表什么
1111 1111 1111 1111 1111 1111 1111 0110 // 这是计算机存储的补码
因为: 补码 = 原码取反 + 1
所以: 原码 = (补码 - 1)取反
1111 1111 1111 1111 1111 1111 1111 0101 // 我们将补码-1得到反码
1000 0000 0000 0000 0000 0000 0000 1010 // 将反码除符号为取反 就是我们正常理解的格式也就是负数的原码也就是-10
同样的我们反过来
~-10
1000 0000 0000 0000 0000 0000 0000 1010 // -10原码
// 我们需要将-10转化为补码才能进行计算机运算,首先取反
1111 1111 1111 1111 1111 1111 1111 0101 // 得到反码
1111 1111 1111 1111 1111 1111 1111 0110 // 加一得到补码
// 现在我们可以进行~运算符运算了将所有位全部取反
0000 0000 0000 0000 0000 0000 0000 1001 // 9
总结: 发现规律其实就是把数变成负数然后减1
应用:
数字类型取整数,运算更加高效,因为其生成的32位是整数存储,所以会直接舍弃小数部分的数值,但是其最高只有存储32位,但是我们js内最高存储位数是53位所以使用的时候需要确定数字的最大位数才行。
~~10.2341 // 10 去除小数
~~true // 1
~~false // 0
~~'' // 0
~~[] // 0
~~undefined // 0
~~!undefined // 1
~~null // 0
~~!null // 1
还可以用来判断是否是-1,因为~-1
等于0
也就是在条件判断语句中是false
,所以在使用findIndex
和 indexOf
的时候非常可以装逼🚀
if(~str.indexOf('test')) {
...当不为-1的情况处理
}
------------------------------
if(!~arr.findIndex('test')) {
...当为-1的情况处理
}
移位运算符
1.<<
“<<”运算符是二进制左移运算,在移动过程中符号位不变,其他的向左移,右侧出现的空位用0填充,超出32位的值丢弃。
3 << 2 // 等于12
0000 0000 0000 0000 0000 0000 0000 0011 // 3
-------------------------------------------- // 整体左移
0000 0000 0000 0000 0000 0000 0000 1100 // 12
2.>>
“>>”运算符执行有符号右移位运算。与左移运算操作相反,它把 32 位数字中的所有有效位整体右移,再使用符号位的值填充空位。移动过程中超出的值将被丢弃,符号位不移动。
13 >> 2 // 等于3
0000 0000 0000 0000 0000 0000 0000 1101 // 13
-------------------------------------------- // 整体右移
0000 0000 0000 0000 0000 0000 0000 0011 // 3
-9 >> 2 // 等于3
1000 0000 0000 0000 0000 0000 0000 1001 // 9
-------------------------------------------- //
1000 0000 0000 0000 0000 0000 0000 0010 // 整体右移
1000 0000 0000 0000 0000 0000 0000 0011 // 用符号位填充得到3
3.>>>
“>>>”运算符执行有符号右移位运算。但是不同的是其一开始生成的32位二进制码中左侧空余的位数也就是0位要全部用符号位填充然后再执行整体右移动,符号位也一起移动
10 >>> 2 // 等于2
0000 0000 0000 0000 0000 0000 0000 1010 // 10
0000 0000 0000 0000 0000 0000 0000 1010 // 用符号位填充因为符号位是0所以没变化
--------------------------------------------
0000 0000 0000 0000 0000 0000 0000 0010 // 整体右移等于2
-10 >>> 2
1000 0000 0000 0000 0000 0000 0000 1010 // 10
1111 1111 1111 1111 1111 1111 1111 1010 // 用符号位填充因为符号位是1
--------------------------------------------
0011 1111 1111 1111 1111 1111 1111 1110 // 最终结果 2**30 - 2
0.1 + 0.2 = ?
在我们了解这个运算之前我们得先了解小数也就是浮点数在我们的计算机内存是如何存储的,我们以1.125为例:
首先将数字分为整数部分和小数部分:
1
0.125
整数部分我们就不多说了,那小数部分是如何用二进制表示呢?看下面:
0.125
------------------ *2
0.25 // 整数部分是0, 所以我们的第一位是0,总共是0
------------------ *2
0.5 // 整数部分是0, 所以我们的第二位是0,总共是00
------------------ *2
1.0 // 整数部分是1, 所以我们的第三位是1,总共是001
自此小数部分被清空,那我们的小数部分0.125就是001,1.125就是 1.001
那我们来看一下0.2用二进制怎么表示:
0.2
------------------ *2
0.4 // 整数部分是0, 所以我们的第一位是0,总共是0
------------------ *2
0.8 // 整数部分是0, 所以我们的第二位是0,总共是00
------------------ *2
1.6 // 整数部分是1, 所以我们的第三位是1,总共是001
------------------ *2
1.2 // 整数部分是1, 所以我们的第三位是1,总共是0011
------------------ *2
0.4 // 整数部分是0, 所以我们的第一位是0,总共是00110
------------------ *2
0.8 // 整数部分是0, 所以我们的第二位是0,总共是0001100
------------------ *2
1.6 // 整数部分是1, 所以我们的第三位是1,总共是00011001
你会发现这是一个永远无法结束的循环,0.1也一样,因为0.2*2后就是变成0.2了这样又是无限循环了,因为在我们的计算机内存中存储浮点数的位数是有限的,所以最终我们会因为小数位数过多而被丢弃多余部分,所以我们其实很多小数在计算机内的存储都是近似值,只能是无限解决,所以一旦涉及到运算,十进制转二进制就会出问题,在转回十进制就会出现丢失精度的问题