爲何不要在 JavaScript 中使用位操做符?

本文最先在個人我的博客《咀嚼之味》發佈:http://jerryzou.comjavascript

若是你的第一門編程語言不是 JavaScript,而是 C++ 或 Java,那麼一開始你大概會看不慣 JavaScript 的數字類型。在 JavaScript 中的數字類型是不區分什麼 Int,Float,Double,Decimal 的。咳咳,我說的固然是在 ES6 以前的 JS,在 ES6 的新標準中提出了像 Int8Array 這樣新的數據類型。不過這不是本文敘述的重點,暫且就不談啦。本文將更着重地談 JS 的數字類型以及做用於它的位操做符,而關於包裝對象 Number 的更多瞭解能夠看拔赤翻譯的《JavaScript設計模式》java

數字類型的本質

實際上,JavaScript的數字類型的本質就是一個基於 IEEE 754 標準的雙精度 64 位的浮點數。按照標準,它的數據結構如圖示這樣:由1位符號位,11位指數部分以及52位尾數部分構成。git

general double float number

在浮點數中,數字一般被表示爲:github

(-1)sign × mantissa × 2exponent編程

而爲了將尾數規格化,並作到儘可能提升精確度,就須要把尾數精確在 [1,2) 的區間內,這樣即可省去前導的1。好比:設計模式

11.101 × 23 = 1.1101 × 24
0.1001 × 25 = 1.001 × 24安全

而且標準規定指數部分使用 0x3ff 做爲偏移量,也就有了雙精度浮點數的通常公式:markdown

(-1)sign × 1.mantissa × 2exponent - 0x3ff數據結構

舉一些例子,應該能幫助你理解這個公式:app

3ff0 0000 0000 0000  =  1
c000 0000 0000 0000  =  -2
3fd5 5555 5555 5555  ~  1/3
0000 0000 0000 0000  =  0
8000 0000 0000 0000  =  -0
7ff0 0000 0000 0000  =  無窮大 ( 1/0 )
fff0 0000 0000 0000  =  負無窮大 ( 1/-0 )
7fef ffff ffff ffff  ~  1.7976931348623157 x 10^308 (= Number.MAX_VALUE)
433f ffff ffff ffff  =  2^53 - 1 (= Number.MAX_SAFE_INTEGER)
c33f ffff ffff ffff  =  -2^53 + 1 (= Number.MIN_SAFE_INTEGER)

得益於尾數省略的一位「1」,使用雙精度浮點數來表示的最大安全整數爲 -253+1 到 253-1 之間,因此若是你僅僅使用 JavaScript 中的數字類型進行一些整數運算,那麼你也能夠近似地將這一數字類型理解爲 53 位整型。

讓人又愛又恨的位操做符

熟悉 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:

  1. Let lref be the result of evaluating A.
  2. Let lval be GetValue(lref).
  3. Let rref be the result of evaluating B.
  4. Let rval be GetValue(rref).
  5. Let lnum be ToInt32(lval).
  6. Let rnum be ToInt32(rval).
  7. 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 中別使用位操做符。

參考資料

  1. 維基百科:雙精度浮點數
  2. MDN:JavaScript數據結構
  3. MDN:按位操做符
  4. How to use bitmask?
  5. ECMAScript® Language Specification - 11.10 Binary Bitwise Operators
相關文章
相關標籤/搜索