20 July 2016
号外号外:专注于移动端的fullPage.js来啦!!!快点我查看

事情的起因是这样的最近在业务代码中发现下面这样的一行代码,我看了半天没搞明白是什么意思,不知道聪明的你能不能知道是什么意思呢?

!~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/

公众号

扫码或搜索:颜海镜,最新博文优先推送
微信公众号:颜海镜


ujian