事情的起因是這樣的最近在業(yè)務(wù)代碼中發(fā)現(xiàn)下面這樣的一行代碼,我看了半天沒搞明白是什么意思,不知道聰明的你能不能知道是什么意思呢?
!~location.href.search('***')
如果你也不知道,并且也像我一樣富有好奇心那么就和我一起來學(xué)習(xí)這篇文章吧。在本文中你將學(xué)到如下知識:
本文假設(shè)你知道計算機中用二進制數(shù)來存儲,計算數(shù)字,并且熟悉二進制數(shù)的表示方法。
為了實現(xiàn)不同的目的,其實都是為了簡化問題,二進制數(shù)在計算機中有不同的表示方法,如原碼、反碼、補碼和移碼等。
注意:本文問了簡化運算,二進制數(shù)都是用一個字節(jié)——8個二進制位來簡化說明
先來說說真值吧,我們表示自然數(shù)包括正數(shù),負(fù)數(shù)和0,下面是1和-1的二進制表示,我們稱為真值
+ 00000001 # +1
- 00000001 # -1
8位二進制數(shù)能表示的真值范圍是[-2^8, +2^8]。
由于計算機只能存儲0和1,不能存儲正負(fù),所以用8個二進制位的最高位來表示符號,0表示正,1表示負(fù),用后七位來表示真值的絕對值,這種表示方法稱為原碼表示法,簡稱原碼,上面的1和-1的原碼如下:
0 0000001 # +1
1 0000001 # -1
由于10000000
的意思是-0,這個沒有意義,所有這個數(shù)字被用來表示-128,所有負(fù)數(shù)就比整數(shù)多一個。
由于最高位被用來表示符號了,現(xiàn)在能表示的范圍是[-2^7, +2^7-1],即[-128, +127]
反碼是另一種表示數(shù)字的方法,其規(guī)則是整數(shù)的反碼何其原碼一樣,負(fù)數(shù)的反碼將其原碼的符號位不變,其余各位按位取反
0 0000001 # +1
1 1111110 # -1
反碼的表示范圍是[-2^7, +2^7-1],即[-128, +127]
補碼是另外一種表示方法,主要是為了簡化運算,將減法變?yōu)榧臃ǘl(fā)明的數(shù)字表示法,其規(guī)則是整數(shù)的補碼和原碼一樣,負(fù)數(shù)的補碼是其反碼末尾加1
0 0000001 # +1
1 1111111 # -1
快速計算負(fù)數(shù)補碼的規(guī)則就是,由其原碼低位向高位找到第一個1,1和其低位不變,1前面的高位按位取反即可,不知道聰明的你能不能想到原理。
8位補碼表示的范圍是[-2^7, +2^7-1],即[-128, +127]
再來說說js中的二進制整數(shù)表示,一名合格的jser應(yīng)該支持在js中只有一種數(shù)字類型,就是浮點型,js的浮點數(shù)遵循IEEE 754規(guī)范,如果你想了解js浮點數(shù)的更多知識,我推薦你看這篇文章《每一個JavaScript開發(fā)者應(yīng)該了解的浮點知識》。
然而在js中還有另一種類型的數(shù)據(jù),那就是用32個比特位表示的整數(shù),只要對js中的任何數(shù)字做位運算操作系統(tǒng)內(nèi)部都會將其轉(zhuǎn)換成整形,嘗試在控制臺輸入下面的代碼
2.1 | 0 # 或運算
>>> 2
js中的這種整形是區(qū)分正負(fù)數(shù)的,我們根據(jù)上面的知識推斷js中的整數(shù)的表示范圍是[-2^31, +2^31-1],即[-2147483648, +2147483647],在控制臺輸出下面的代碼來驗證我們的推斷
-2147483648 | 0
>>> -2147483648
-2147483649 | 0
>>> 2147483647
2147483647 | 0
>>> 2147483647
2147483648 | 0
>>> -2147483648
從上面的結(jié)果可以看出,大于和小于最低和最高的值再去進行轉(zhuǎn)換時都將改變正負(fù)號
js中的位運算符有下面這些,對數(shù)字進行這些操作時,系統(tǒng)內(nèi)部都會講64的浮點數(shù)轉(zhuǎn)換成32位的整形
下面舉例子來說明每個運算符的作用,開始之前先來介紹幾個會用到的知識點
es6中引入了原生二進制字面量,二進制數(shù)的語法是0b開頭,我們將會用到這個新功能,目前chrome最新版已經(jīng)支持。
0b111 // 7
0b001 // 1
先來介紹下下面會用到的一個方法——Number.prototype.toString方法可以講數(shù)字轉(zhuǎn)化為字符串,有一個可選的參數(shù),用來決定將數(shù)字顯示為指定的進制,下面可以查看3的二進制表示,根據(jù)這個特性,我還特意做了一個進制轉(zhuǎn)化工具。
3..toString(2)
>> 11
&按位與會將操作數(shù)和被操作數(shù)的相同為進行與運算,如果都為1則為1,如果有一個為0則為0
101
011
---
001
101和011與完的結(jié)果就是001,下面在js中進行驗證
(0b101 & 0b011).toString(2)
>>> "1"
|按位或是相同的位置上只要有一個為1就是1,兩個都為0則為0
101
001
---
101
101和001或完的結(jié)果是101,下面在js中進行驗證
(0b101 | 0b001).toString(2)
>>> "101"
~操作符會將操作數(shù)的每一位取反,如果是1則變?yōu)?,如果是0則邊為1
101
---
010
101按位非的結(jié)果是010,下面在js中驗證
(~0b101).toString(2)
>>> "-110"
啊呀,怎么結(jié)果不對呢?。?!上面提到了js中的數(shù)字是有符號的,我們忘記了最高位的符號了,為了簡化我們將32位簡化為8位,注意最高位是符號位
0 0000101
1 1111010 // 非后的結(jié)果
1 0000101 // 求反
1 0000110 // 求補
1 1111010
明顯是一個負(fù)數(shù),而且是負(fù)數(shù)的補碼表示,我們的求它的原碼,也就是再對它求補1 0000110
就是這個數(shù)的真值,也就是結(jié)果顯示-110,這下總算自圓其說了,O(∩_∩)O哈哈~
其實上面的與和或也都是會操作符號位的,不信你試試下面這兩個,可以看到符號位都參與了運算
(0b1&-0b1)
>>> 1
(0b1|-0b1)
>>> -1
再來說說異或,這個比較有意思,異或顧名思義看看兩個位是否為異——不同,兩個位不同則為1,兩個位相同則為0
101
001
---
100
101和001異或的結(jié)果是100,js中驗證
(0b101^0b001).toString(2)
>>> "100"
左移的規(guī)則就是每一位都向左移動一位,末尾補0,其效果相當(dāng)于×2,其實計算機就是用移位操作來計算乘法的
010
---
0100
010左移一位就會變?yōu)?00,下面在js中驗證
(0b010<<1).toString(2)
>>> "100"
算數(shù)右移也稱為有符號右移,也就是移位的時候高位補的是其符號位,整數(shù)則補0,負(fù)數(shù)則補1
(0b111>>1).toString(2)
>>> "11"
(-0b111>>1).toString(2)
>>> "-100"
負(fù)數(shù)的結(jié)果好像不太對勁,我們來看看是怎么回事
-111 // 真值
1 0000111 // 原碼
1 1111001 // 補碼
1 1111100 // 算數(shù)右移
1 0000100 // 移位后的原碼
-100 // 移位后的真值
邏輯右移又稱為無符號右移,也就是右移的時候高位始終補0,對于整數(shù)和算數(shù)右移沒有區(qū)別
(0b111>>>1).toString(2)
>>> "11"
對于負(fù)數(shù)則就不同了,右移后會變?yōu)檎龜?shù)
(-0b111>>>1).toString(2)
>>> "1111111111111111111111111111100"
關(guān)于二進制數(shù)就說這么多吧,再來說說開頭的問題,開頭的問題其實可以分解為下面的問題因為search會返回-1 和找到位置的索引,也就成了下面的問題
!~-1
>>> ture
!~0
>>> false
!~1
>>> false
非運算對于數(shù)字的結(jié)果相當(dāng)于改變符號,并對其值的絕對值-1
~-1
>>> 0
~0
>>> -1
~1
>>> -2
其實可以看出!~x的邏輯就是判斷x是否為-1,my god這邏輯真是逆天了,我還是勸大家直接寫成 x === -1多好啊
通過這篇文章終于把當(dāng)年沒學(xué)明白的二進制數(shù)搞明白了,希望你和我一樣,祝你好運。
更多建議: