晚上在讀 lodash 源碼的時候,看到 baseSlice 中有這樣一行代碼:
start >>>= 0;
這不就是無符號右移嘛,當時第一感覺是是為了取絕對值,后來發現并不是,嘗試了多次之后,發現情況有點詭異啊,我們使用 chrome 調試工具運行一下 js 中的無符號右移 0 位。
不僅是 null 無符號右移會變成 0 ,js 中的其他非數值做此運算都會變成 0 。
接下來我們來看看為什么會這樣(事實上不僅僅只是無符號右移是這樣)。要理解這個問題需要先明白什么是位運算以及為什么需要位運算,然后搞明白 js 中的位運算有什么特別之處。
什么是位運算?
敬請期待
js 中的位運算有什么特別之處呢?
(這一部分我是拿 java、go 與 js 做對比的。)
1 js 中為什么浮點數也能參與位運算
這在 java、go、c 中都是不被允許的
* 6 種 基本類型:
1. Boolean
2. Null
3. Undefined
4. Number
5. String
6. Symbol (ECMAScript 6 新定義)
細心的人已經發現,基本類型里并沒有浮點型。
事實上在 js 中的 Number 類型是不區分 int、long、float、double 類型的(go 的用戶們就呵呵一笑了,來來來,我們的浮點型就能王炸你)。回正題,不區分整型、浮點型那怎么存儲呢,為了不丟失精度, js 中的 Number 類型實際上一個基于 IEEE 754 標準的雙精度64位浮點數(java 的同學就把它當成 double 看)。看到這我想很多人應該能明白為什么 js 里浮點數也能參與位運算了吧。這也是沒有辦法,因為對于內存來說整型、浮點型都沒有區別了。
這里是有一個問題的,因為當 js 需要進行位運算時,會將操作數通通轉成 32 位比特序列(0,1),也就是補碼。操作完成之后,再按照 64 位浮點數存儲
2 那么 js 做位運算時,小數部分怎么處理呢?
注意這里說的全部位運算
直接丟棄!!! 曾吶!這么虎?
沒錯,就是這么暴力,那么問題來了,既然小數部分不參與位運算,那么為什么不能像 java、go 那樣直接禁止呢?關于這個問題,我想那就是語言設計者的想法,我就不知道了。但是這其實也帶來了一些特別的操作,比如在 js 中雙取反是可以做取整操作的。
~~2.2 // 2
~~2.8 // 2
3 js 中非數值類型如何進行位運算的呢?
當 js 需要進行位運算的時候,對于非數值類型,會首先將操作數轉成一個整型(就是0)然后在進行運算。這就解釋了為什么 js 中可以允許非數值類型參與運算,其實這是個偽命題,因為實質上是對非數值操作數的整型表達式進行的位運算。
這里需要注意,上面說過了 js 中的整型在內存中都是一個 64 位雙精度浮點型,但是 js 進行位運算時,會將操作數轉成帶符號位的 32 位比特序列(0,1),也就是補碼。運算結束后,再按照 64 位存儲。那么問題來了,這里肯定會存在精度丟失對吧,這應該不難理解。js 確實也是這樣處理的,超過 32 位的部分直接截斷。
所以對一個非數值變量做取反操作,得到的一定是 -1,因為實際上等于對 0 做取反操作。
4 js 中的無符號右移到底有什么特別之處?
首尾呼應一下,畢竟就是這個問題使我查資料寫了這篇文章。
首先解釋一下,>>> 無符號右移原本是 java 里特有的(這里是和 js、go 對比,其他語言我沒用過,不能亂說)。js 中的無符號右移跟 java 幾乎一樣,除了一點兩種語言處理方式完全不一樣。
那就是并沒有真正發生移位的情況下,符號位會不會被替換成0。java 中是不會替換的,但是 js 中是會發生替換的。
當操作數是正數的時候,不管有沒有真的移位并沒有區別,因為正數的符號位是 0。
當操作數是負數時,移動位數大于0,也體現不出區別:
// java
5 >>> 0 // 5
5 >>> 1 // 2
-1 >>> 1 // 2147483647
// js
5 >>> 0 // 5
5 >>> 1 // 2
-1 >>> 1 // 2147483647
但是當操作數是負數,無符號右移 0 位時,區別就大了:
// java
-1 >>> 0 // -1
// js
-1 >>> 0 // 4294967295
這是因為 -1 的補碼是:
11111111111111111111111111111111
>>>0 實際上并沒有發生數位變化,但是 js 卻會把符號位替換成 0,
// java
11111111111111111111111111111111
// js
01111111111111111111111111111111
此時原來負數的補碼,變為了正數的源碼(這就是為什么 js 中 -1>>>0 會變成一個巨大的正整數)。
js 中無符號右移時,不管正數、負數都會首先將符號位替換成 0,然后再進行移位。也就是說,該運算符永遠返回正整數。
總結
js 的位運算,為什么會有這么多奇怪的地方呢?我相信很多同學都會有這種想法,特別是 java 的同學們吧。為此我查了 js 的歷史。
1995 Sun 公司正式發布 java 語言,當時的網景公司正在為它們的 Navigator 瀏覽器尋找一種網頁腳本(此前的瀏覽器不具備互動能力)。當他們看到 Sun 公司的宣傳后,與 Sun 合作開發全新的腳本語言 javascript 。此前我一直不明白 js 既然不是 java 的腳本,為什么叫這個名字。現在懂了,因為當時新腳本語言的決策中,Sun 公司占了很大一環。
1995年5月 按照公司的要求(一個像 java 但是比 java 簡單的腳本語言),Brendan Eich 僅用10天就寫出了 javascript。
在我們膜拜大神的時候,也要認清一個現實,當時給 Brendan Eich 的時間太短了,所以很多問題并沒有很好的解決,而且一邊模仿 java、c,一邊還要簡化數據類型、內存模型。我覺得這就是為什么 js 的位運算這么奇怪的原因。
js 完全套用了 java 的位運算符。
但是 java 的位運算是針對整數的,對 js 沒什么用啊,因為 js 中,所有數字都保存為雙精度浮點型。如果使用它們的話,js 不得不將操作數先轉為整數,然后再進行運算。
所以很多人不建議在 js 中使用位運算,理由是 js 天生就會進行類型轉換,使得效率降低。