事情的起因是这样的最近在业务代码中发现下面这样的一行代码,我看了半天没搞明白是什么意思,不知道聪明的你能不能知道是什么意思呢?
!~location.href.search('***')
如果你也不知道,并且也像我一样富有好奇心那么就和我一起来学习这篇文章吧。在本文中你将学到如下知识:
- 二进制数的表示
- js中的二进制数整数
- js中的位运算
二进制数
本文假设你知道计算机中用二进制数来存储,计算数字,并且熟悉二进制数的表示方法。
为了实现不同的目的,其实都是为了简化问题,二进制数在计算机中有不同的表示方法,如原码、反码、补码和移码等。
注意:本文问了简化运算,二进制数都是用一个字节——8个二进制位来简化说明
先来说说真值吧,我们表示自然数包括正数,负数和0,下面是1和-1的二进制表示,我们称为真值
+ 00000001 # +1
- 00000001 # -1
8位二进制数能表示的真值范围是[-2^8, +2^8]。
由于计算机只能存储0和1,不能存储正负,所以用8个二进制位的最高位来表示符号,0表示正,1表示负,用后七位来表示真值的绝对值,这种表示方法称为原码表示法,简称原码,上面的1和-1的原码如下:
0 0000001 # +1
1 0000001 # -1
由于10000000
的意思是-0,这个没有意义,所有这个数字被用来表示-128,所有负数就比整数多一个。
由于最高位被用来表示符号了,现在能表示的范围是[-2^7, +2^7-1],即[-128, +127]
反码是另一种表示数字的方法,其规则是整数的反码何其原码一样,负数的反码将其原码的符号位不变,其余各位按位取反
0 0000001 # +1
1 1111110 # -1
反码的表示范围是[-2^7, +2^7-1],即[-128, +127]
补码是另外一种表示方法,主要是为了简化运算,将减法变为加法而发明的数字表示法,其规则是整数的补码和原码一样,负数的补码是其反码末尾加1
0 0000001 # +1
1 1111111 # -1
快速计算负数补码的规则就是,由其原码低位向高位找到第一个1,1和其低位不变,1前面的高位按位取反即可,不知道聪明的你能不能想到原理。
8位补码表示的范围是[-2^7, +2^7-1],即[-128, +127]
js中的二进制数整数
再来说说js中的二进制整数表示,一名合格的jser应该支持在js中只有一种数字类型,就是浮点型,js的浮点数遵循IEEE 754规范,如果你想了解js浮点数的更多知识,我推荐你看这篇文章《每一个JavaScript开发者应该了解的浮点知识》。
然而在js中还有另一种类型的数据,那就是用32个比特位表示的整数,只要对js中的任何数字做位运算操作系统内部都会将其转换成整形,尝试在控制台输入下面的代码
2.1 | 0 # 或运算
>>> 2
js中的这种整形是区分正负数的,我们根据上面的知识推断js中的整数的表示范围是[-2^31, +2^31-1],即[-2147483648, +2147483647],在控制台输出下面的代码来验证我们的推断
-2147483648 | 0
>>> -2147483648
-2147483649 | 0
>>> 2147483647
2147483647 | 0
>>> 2147483647
2147483648 | 0
>>> -2147483648
从上面的结果可以看出,大于和小于最低和最高的值再去进行转换时都将改变正负号
js中的位运算
js中的位运算符有下面这些,对数字进行这些操作时,系统内部都会讲64的浮点数转换成32位的整形
- & 与
- | 或
- ~ 非
- ^ 异或
- << 左移
- >> 算数右移(有符号右移)
- >>> 逻辑右移(无符号右移)
下面举例子来说明每个运算符的作用,开始之前先来介绍几个会用到的知识点
原生二进制字面量
es6中引入了原生二进制字面量,二进制数的语法是0b开头,我们将会用到这个新功能,目前chrome最新版已经支持。
0b111 // 7
0b001 // 1
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
101
---
010
101按位非的结果是010,下面在js中验证
(~0b101).toString(2)
>>> "-110"
啊呀,怎么结果不对呢!!!上面提到了js中的数字是有符号的,我们忘记了最高位的符号了,为了简化我们将32位简化为8位,注意最高位是符号位
0 0000101
1 1111010 // 非后的结果
1 0000101 // 求反
1 0000110 // 求补
1 1111010
明显是一个负数,而且是负数的补码表示,我们的求它的原码,也就是再对它求补1 0000110
就是这个数的真值,也就是结果显示-110,这下总算自圆其说了,O(∩_∩)O哈哈~
其实上面的与和或也都是会操作符号位的,不信你试试下面这两个,可以看到符号位都参与了运算
(0b1&-0b1)
>>> 1
(0b1|-0b1)
>>> -1
^ 异或
再来说说异或,这个比较有意思,异或顾名思义看看两个位是否为异——不同,两个位不同则为1,两个位相同则为0
101
001
---
100
101和001异或的结果是100,js中验证
(0b101^0b001).toString(2)
>>> "100"
<< 左移
左移的规则就是每一位都向左移动一位,末尾补0,其效果相当于×2,其实计算机就是用移位操作来计算乘法的
010
---
0100
010左移一位就会变为100,下面在js中验证
(0b010<<1).toString(2)
>>> "100"
>> 算数右移(有符号右移)
算数右移也称为有符号右移,也就是移位的时候高位补的是其符号位,整数则补0,负数则补1
(0b111>>1).toString(2)
>>> "11"
(-0b111>>1).toString(2)
>>> "-100"
负数的结果好像不太对劲,我们来看看是怎么回事
-111 // 真值
1 0000111 // 原码
1 1111001 // 补码
1 1111100 // 算数右移
1 0000100 // 移位后的原码
-100 // 移位后的真值
>>> 逻辑右移(无符号右移)
逻辑右移又称为无符号右移,也就是右移的时候高位始终补0,对于整数和算数右移没有区别
(0b111>>>1).toString(2)
>>> "11"
对于负数则就不同了,右移后会变为正数
(-0b111>>>1).toString(2)
>>> "1111111111111111111111111111100"
关于开头的问题
关于二进制数就说这么多吧,再来说说开头的问题,开头的问题其实可以分解为下面的问题因为search会返回-1 和找到位置的索引,也就成了下面的问题
!~-1
>>> ture
!~0
>>> false
!~1
>>> false
非运算对于数字的结果相当于改变符号,并对其值的绝对值-1
~-1
>>> 0
~0
>>> -1
~1
>>> -2
其实可以看出!~x的逻辑就是判断x是否为-1,my god这逻辑真是逆天了,我还是劝大家直接写成 x === -1多好啊
总结
通过这篇文章终于把当年没学明白的二进制数搞明白了,希望你和我一样,祝你好运。
参考资料
原文网址:http://yanhaijing.com/javascript/2016/07/20/binary-in-js/