为何不要在 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
相关文章
相关标签/搜索