儘量不要在JS中使用位運算
熟悉 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 beToInt32(lval).
- Let rnum beToInt32(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 中別使用位操作符。