20 July 2016
号外号外:我的新书《现代JavaScript库开发:原理、技术与实战》出版啦!!!快点我查看
号外号外:一组小而美的JavaScript迷你库!!!快点我查看
号外号外:猿辅导招聘前端,后端,客户端啦!地点:北京!!!快点我查看

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

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

微信公众号:颜海镜
关注微信公众号 颜海镜
微信支付二维码
赞赏支持 微信扫一扫


Marsoln

Marsoln"hentai on 21 Jul 2016

不得不说奇技淫巧我就服你们 哈哈

我大爷

我大爷 on 21 Jul 2016

666

颜海镜

颜海镜 on 21 Jul 2016

可怜

颜海镜

颜海镜 on 21 Jul 2016

666

茵风泳月

茵风泳月 on 21 Jul 2016

比较古老的“技巧”了,字符比较少,还有类似的把 true 写成 !0 ,不过uglify默认就做到后者,前者也算一个持续优化点

颜海镜

颜海镜 on 21 Jul 2016

这种工作应该交给 uglify来做

0zonxin0

0zonxin0 on 22 Jul 2016

原来大神才搞明白呀!

颜海镜

颜海镜 on 22 Jul 2016

O(∩_∩)O哈哈~

mqliutie

mqliutie on 25 Jul 2017

1 1111010明显是一个负数,而且是负数的补码表示,我们的求它的原码,也就是再对它求补1 0000110就是这个数的真值,也就是结果显示-110,这下总算自圆其说了,O(∩_∩)O哈哈~

怎么看出来的是补码? 文字里面是不是漏了一个求反的过程?

lyhper

lyhper on 24 Feb 2018

8位无符号二进制数的取值范围应该是[-2^8+1, 2^8-1]

zsjun

zsjun on 29 Dec 2018

@mqliutie 因为负数在计算机里边是用补码表示的

zsjun

zsjun on 29 Dec 2018

@mqliutie 因为负数在计算机用补码表示的