为什么不要在 JavaScript 中使用位操作符?
如果你的第一门编程语言不是 JavaScript,而是 C++ 或 Java,那么一开始你大概会看不惯 JavaScript 的数字类型。在 JavaScript 中的数字类型是不区分什么 Int,Float,Double,Decimal 的。咳咳,我说的当然是在 ES6 之前的 JS,在 ES6 的新标准中提出了像 Int8Array 这样新的数据类型。不过这不是本文叙述的重点,暂且就不谈啦。本文将更着重地谈 JS 的数字类型以及作用于它的位操作符,而关于包装对象 Number 的更多了解可以看拔赤翻译的《JavaScript设计模式》
数字类型的本质
实际上,JavaScript的数字类型的本质就是一个基于 IEEE 754 标准的双精度 64 位的浮点数。按照标准,它的数据结构如图示这样:由1位符号位,11位指数部分以及52位尾数部分构成。
在浮点数中,数字通常被表示为:
(-1)sign × mantissa × 2exponent
而为了将尾数规格化,并做到尽量提高精确度,就需要把尾数精确在 [1,2)
的区间内,这样便可省去前导的1。比如:
11.101 × 23 = 1.1101 × 240.1001 × 25 = 1.001 × 24
并且标准规定指数部分使用 0x3ff 作为偏移量,也就有了双精度浮点数的一般公式:
(-1)sign × 1.mantissa × 2exponent - 0x3ff
举一些例子,应该能帮助你理解这个公式:
3ff0 0000 0000 0000 = 1c000 0000 0000 0000 = -23fd5 5555 5555 5555 ~ 1/30000 0000 0000 0000 = 08000 0000 0000 0000 = -07ff0 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 // 1000unsigned char flags;flags = LOG_ERRORS; // 0001flags = LOG_ERRORS | LOG_WARNINGS | LOG_INCOMING; // 1011
因为标志位一般只需要 1 bit,就可以保存,并没有必要为每个标志位都定义一个变量。所以按上面这种方式只使用一个变量,却可以保存大量的信息——无符号的 char 可以保存 8 个标志位,而无符号的 int 则可以同时表示 32 个标志位。
可惜位操作符在 JavaScript 中的表现就比较诡异了,因为 JavaScript 没有真正意义上的整型。看看如下代码的运行结果吧:
var a, b;a = 2e9; // 2000000000a << 1; // -294967296// fxck!我只想装了个逼用左移1位给 a * 2,但是结果是什么鬼!!!a = parseInt('100000000', 16); // 4294967296b = parseInt('1111', 2); // 15a | 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 中别使用位操作符。
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法