熟悉 C 或者 C++ 的同學一定對位操作符不陌生。位操作符最主要的應用大概就是作為標志位與掩碼。這是一種節省存儲空間的高明手段,在曾經內存的大小以 KB 為單位計算時,每多一個變量就是一份額外的開銷。而使用位操作符的掩碼則在很大程度上緩解了這個問題:
#define LOG_ERRORS 1 // 0001 #define LOG_WARNINGS 2 // 0010 #define LOG_NOTICES 4 // 0100 #define LOG_INCOMING 8 // 1000 unsigned char flags; flags = LOG_ERRORS; // 0001 flags = LOG_ERRORS | LOG_WARNINGS | LOG_INCOMING; // 1011
因為標志位一般只需要 1 bit,就可以保存,並沒有必要為每個標志位都定義一個變量。所以按上面這種方式只使用一個變量,卻可以保存大量的信息——無符號的 char 可以保存 8 個標志位,而無符號的 int 則可以同時表示 32 個標志位。
可惜位操作符在 JavaScript 中的表現就比較詭異了,因為 JavaScript 沒有真正意義上的整型。看看如下代碼的運行結果吧:
var a, b; a = 2e9; // 2000000000 a << 1; // -294967296 // fxck!我只想裝了個逼用左移1位給 a * 2,但是結果是什么鬼!!! a = parseInt('100000000', 16); // 4294967296 b = parseInt('1111', 2); // 15 a | b; // 15 // 啊啊啊,為毛我的 a 絲毫不起作用,JavaScript真是門吊詭的語言!!!
好吧,雖然我說過大家可以近似地認為,JS 的數字類型可以表示 53 位的整型。但事實上,位操作符並不是這么認為的。在 ECMAScript® Language Specification 中是這樣描述位操作符的:
The production A : A @ B, where @ is one of the bitwise operators in the productions above, is evaluated as follows:
- Let lref be the result of evaluating A.
- Let lval be GetValue(lref).
- Let rref be the result of evaluating B.
- Let rval be GetValue(rref).
- Let lnum be ToInt32(lval).
- Let rnum be ToInt32(rval).
- Return the result of applying the bitwise operator @ to lnum and rnum. The result is a signed 32 bit integer.
需要注意的是第5和第6步,按照ES標准,兩個需要運算的值會被先轉為有符號的32位整型。所以超過32位的整數會被截斷,而小數部分則會被直接舍棄。
而反過來考慮,我們在什么情況下需要用到位操作符?使用左移來代替 2 的冪的乘法?Naive啊,等遇到像第一個例子的問題,你就要抓狂了。而且對一個浮點數進行左移操作是否比直接乘 2 來得效率高,這也是個值得商榷的問題。
那用來表示標志位呢?首先,現在的內存大小已經不值得我們用精簡幾個變量來減少存儲空間了;其次呢,使用標志位也會使得代碼的可讀性大大下降。再者,在 JavaScript 中使用位操作符的地方畢竟太少,如果你執意使用位操作符,未來維護這段代碼的人又對 JS 中的位操作符的坑不熟悉,這也會造成不利的影響。
所以,我對大家的建議是,盡量在 JavaScript 中別使用位操作符。