晚上在读 lodash 源码的时候,看到 baseSlice 中有这样一行代码:
start >>>= 0;
这不就是无符号右移嘛,当时第一感觉是是为了取绝对值,后来发现并不是,尝试了多次之后,发现情况有点诡异啊,我们使用 chrome 调试工具运行一下 js 中的无符号右移 0 位。
不仅是 null 无符号右移会变成 0 ,js 中的其他非数值做此运算都会变成 0 。
接下来我们来看看为什么会这样(事实上不仅仅只是无符号右移是这样)。要理解这个问题需要先明白什么是位运算以及为什么需要位运算,然后搞明白 js 中的位运算有什么特别之处。
什么是位运算?
敬请期待
js 中的位运算有什么特别之处呢?
(这一部分我是拿 java、go 与 js 做对比的。)
1 js 中为什么浮点数也能参与位运算
这在 java、go、c 中都是不被允许的
* 6 种 基本类型:
1. Boolean
2. Null
3. Undefined
4. Number
5. String
6. Symbol (ECMAScript 6 新定义)
细心的人已经发现,基本类型里并没有浮点型。
事实上在 js 中的 Number 类型是不区分 int、long、float、double 类型的(go 的用户们就呵呵一笑了,来来来,我们的浮点型就能王炸你)。回正题,不区分整型、浮点型那怎么存储呢,为了不丢失精度, js 中的 Number 类型实际上一个基于 IEEE 754 标准的双精度64位浮点数(java 的同学就把它当成 double 看)。看到这我想很多人应该能明白为什么 js 里浮点数也能参与位运算了吧。这也是没有办法,因为对于内存来说整型、浮点型都没有区别了。
这里是有一个问题的,因为当 js 需要进行位运算时,会将操作数通通转成 32 位比特序列(0,1),也就是补码。操作完成之后,再按照 64 位浮点数存储
2 那么 js 做位运算时,小数部分怎么处理呢?
注意这里说的全部位运算
直接丢弃!!! 曾呐!这么虎?
没错,就是这么暴力,那么问题来了,既然小数部分不参与位运算,那么为什么不能像 java、go 那样直接禁止呢?关于这个问题,我想那就是语言设计者的想法,我就不知道了。但是这其实也带来了一些特别的操作,比如在 js 中双取反是可以做取整操作的。
~~2.2 // 2
~~2.8 // 2
3 js 中非数值类型如何进行位运算的呢?
当 js 需要进行位运算的时候,对于非数值类型,会首先将操作数转成一个整型(就是0)然后在进行运算。这就解释了为什么 js 中可以允许非数值类型参与运算,其实这是个伪命题,因为实质上是对非数值操作数的整型表达式进行的位运算。
这里需要注意,上面说过了 js 中的整型在内存中都是一个 64 位双精度浮点型,但是 js 进行位运算时,会将操作数转成带符号位的 32 位比特序列(0,1),也就是补码。运算结束后,再按照 64 位存储。那么问题来了,这里肯定会存在精度丢失对吧,这应该不难理解。js 确实也是这样处理的,超过 32 位的部分直接截断。
所以对一个非数值变量做取反操作,得到的一定是 -1,因为实际上等于对 0 做取反操作。
4 js 中的无符号右移到底有什么特别之处?
首尾呼应一下,毕竟就是这个问题使我查资料写了这篇文章。
首先解释一下,>>> 无符号右移原本是 java 里特有的(这里是和 js、go 对比,其他语言我没用过,不能乱说)。js 中的无符号右移跟 java 几乎一样,除了一点两种语言处理方式完全不一样。
那就是并没有真正发生移位的情况下,符号位会不会被替换成0。java 中是不会替换的,但是 js 中是会发生替换的。
当操作数是正数的时候,不管有没有真的移位并没有区别,因为正数的符号位是 0。
当操作数是负数时,移动位数大于0,也体现不出区别:
// java
5 >>> 0 // 5
5 >>> 1 // 2
-1 >>> 1 // 2147483647
// js
5 >>> 0 // 5
5 >>> 1 // 2
-1 >>> 1 // 2147483647
但是当操作数是负数,无符号右移 0 位时,区别就大了:
// java
-1 >>> 0 // -1
// js
-1 >>> 0 // 4294967295
这是因为 -1 的补码是:
11111111111111111111111111111111
>>>0 实际上并没有发生数位变化,但是 js 却会把符号位替换成 0,
// java
11111111111111111111111111111111
// js
01111111111111111111111111111111
此时原来负数的补码,变为了正数的源码(这就是为什么 js 中 -1>>>0 会变成一个巨大的正整数)。
js 中无符号右移时,不管正数、负数都会首先将符号位替换成 0,然后再进行移位。也就是说,该运算符永远返回正整数。
总结
js 的位运算,为什么会有这么多奇怪的地方呢?我相信很多同学都会有这种想法,特别是 java 的同学们吧。为此我查了 js 的历史。
1995 Sun 公司正式发布 java 语言,当时的网景公司正在为它们的 Navigator 浏览器寻找一种网页脚本(此前的浏览器不具备互动能力)。当他们看到 Sun 公司的宣传后,与 Sun 合作开发全新的脚本语言 javascript 。此前我一直不明白 js 既然不是 java 的脚本,为什么叫这个名字。现在懂了,因为当时新脚本语言的决策中,Sun 公司占了很大一环。
1995年5月 按照公司的要求(一个像 java 但是比 java 简单的脚本语言),Brendan Eich 仅用10天就写出了 javascript。
在我们膜拜大神的时候,也要认清一个现实,当时给 Brendan Eich 的时间太短了,所以很多问题并没有很好的解决,而且一边模仿 java、c,一边还要简化数据类型、内存模型。我觉得这就是为什么 js 的位运算这么奇怪的原因。
js 完全套用了 java 的位运算符。
但是 java 的位运算是针对整数的,对 js 没什么用啊,因为 js 中,所有数字都保存为双精度浮点型。如果使用它们的话,js 不得不将操作数先转为整数,然后再进行运算。
所以很多人不建议在 js 中使用位运算,理由是 js 天生就会进行类型转换,使得效率降低。