引言
如果你是一個硬件工程師,那么你對位運算相關的東西必然已經非常熟悉,但作為一個前端工程師的你呢?這一點上,確實比較讓人疑惑。當我在學校里,還在搞硬件相關的東西的時候,一方面的原因是當初的應用場景很多時候都會接觸到,而另一方面,當初使用的編程語言并沒有如同 JavaScript 一般靈活方便。考慮到 ROM/RAM 等因素,我如果想要依次點亮一排流水燈的時候,我可能會使用位移運算符配合一個數字來標記哪顆燈應該被點亮;當我需要存儲多個布爾類型的數據的時候,我可能使用掩碼的形式,存儲和使用需要的信息。但是當我習慣了 JavaScript 提供給我的便利以及運行環境提供給我更大的容量、更快的計算速度的時候,不知不覺間,我竟漸漸忘了之前的編程方式。所以,不如提醒一下自己。
二進制與位運算
雖然現代瀏覽器、Nodejs 等為開發者提供了強大的 JavaScript 運行環境,但是這些環境的核心仍舊是運行在二進制的基礎之上的,毋庸置疑的是,基于二進制數據在運行位運算的時候遠比其它數學運算、布爾操作等快得多。JavaScript 為開發者提供了多種位運算支持:
- 按位與(AND,&)
當兩個操作數對應位數都是 1 時,則在該位返回 1,否則則在該位返回 0;
1 & 4 // 0, 1 & 100 -> 000
3 & 5 // 1, 11 & 101 -> 001
- 按位或(OR,|)
當兩個操作數對應位數中至少有一個是 1 是,則在該位返回 1,否則則在該位返回 0;
1 | 4 // 5, 1 | 100 -> 101
3 | 5 // 7, 11 | 101 -> 111
- 按位異或(XOR,^)
當兩個操作數對應位數中兩個數中,一個為 1,一個為 00,則在該位返回 1,否則則在該位返回;
1 ^ 4 // 5, 1 ^ 100 -> 101
3 ^ 5 // 6, 11 ^ 101 -> 110
- 按位取反(NOT,~)
在操作數對應位置,是 1 則在該位返回 0,否則則在該位返回 1;
~1 // -2, 因為補碼存在的原因,這里可能和期望的有所不同,有興趣可以深入學習一下
- 移位運算 (>>, <<, >>>, <<<)
5 >> 1 // 2, 101 >> 1 -> 010
3 << 1 // 6, 11 << 1 -> 110
......
利用位運算進行性能優化
(一) 代替數學運算
很多情況下,我們可以使用位運算替代數學運算操作,比如判斷一個數是否是奇數,最典型的做法當然是對 2 求余數,也即 if (value % 2) {}
。但與此同時,二進制奇數的最低位一定是 1,這樣我們就能通過如按位與的方式完成我們的需求,if (value & 1) {}
。雖然改動比較小,但這個計算速度卻將因此得到很大的提高。
(二) 位掩碼
位掩碼在一些偏底層的語言中使用得非常廣泛,但 JavaScript 中卻似乎被大眾給忽略了,更多的情況下,大家都在關注業務、功能和兼容性等方面的問題,運行環境中提供的便利的 api 似乎也讓我們覺得沒太大必要使用掩碼技術,但性能要求較高的場景中,我們還是會嘗試從一些經典的編程范式中尋找答案。用一個很簡單的例子來說明位掩碼技術:
// 定義所有的可選項
var OPTION_A = 1,
OPTION_B = 2,
OPTION_C = 4,
OPTION_D = 8,
OPTION_E = 16;
// 包含選項 A、B、D
var options = OPTION_A | OPTION_B | OPTION_D; // 1 | 10 | 1000 -> 1011
// 判斷選項是否被包含在 options 中
options & OPTION_A // 1011 & 1 -> 0001, true
options & OPTION_B // 1011 & 10 -> 0010, true
options & OPTION_C // 1011 & 110 -> 0, false
options & OPTION_D // 1011 & 1000 -> 1000, true
options & OPTION_E // 1011 & 10000 -> 0, false
之前,你可能會使用比如數組的 indexOf 或者 Map 數據結構等方式來確認一個數據在不在你的數據集合中,但相比而言,掩碼的運算速度遠比它們高,除卻函數調用帶來額外開銷,位運算發生于系統底層,尤其是如果有多個選項保存在一起并需要頻繁檢查的時候,位掩碼將大放異彩。
位運算的作用遠不止如此,我們整個計算機系統都是在其基礎之上構建出來的,這里羅列的只是冰山一角,歡迎大家幫我補充~~~