前言
最近回顾javascript
的一些基础知识点时,引起的思考确实颠覆了我之前的一些认知。我清楚地记得曾多次在网上看到一些奇奇怪怪的表达式,它们的运算结果着实让人懵逼。就比如我在js数据类型很简单,却也不简单这一篇笔记中提到的[] == ![]
这样一个表达式,它的运算结果是true
。如果你不细致地去研究它背后的运算逻辑,你只会惊呼”这是什么鬼“?相反,当你静下心来看清楚它的运算逻辑后,你会感叹“妙哉妙哉”!没错,本文的主角就是这些容易让人小觑的运算符。
加法运算符+
首先说的是加法运算符+
,这是一个很容易被人忽视的运算符。我们知道,+
可以用来做数字运算,也可以用作字符串拼接,但是还有一些细节可能是大家不知道的。如果+
运算符的两个操作数类型不一致,或者说两个操作数既不是字符串也不是数字,那么它的运算规则是什么?
先举几个例子,你可以先思考下这些运算结果分别是什么。
1 | var a = 1 + "1"; |
其实规则很简单,我们只要简单地列举出数据类型的可能性,就几乎得到了完整的答案。
- 如果操作数都是数字,进行数字的加法运算。
- 如果操作数都是字符串,进行字符串的拼接。
- 如果操作数是对象,会转换为原始值(一般是先调用
valueOf()
,日期对象比较特殊,会调用toString()
),得到的原始值不再被强制转换为数字或字符串。在这种约束下,对象转为原始值基本都是字符串(如果你没有重写valuOf()
或者toString()
方法),根据下面的第四点,会执行字符串拼接操作。 - 如果其中一个操作数是字符串,另一个操作数也会被转为字符串,
+
运算符执行字符串拼接操作。 - 如果两个操作数都不是字符串或对象,则会进行算术加法运算(非数字的操作数会被强制转为数字)。
所以,不难得出上面列举的表达式的运算结果。
1 | var a = 1 + "1"; // "11" |
要记住这些规则并不简单,一个记忆技巧是:+
运算符偏爱字符串拼接操作。
相等运算符==
这个运算符的运算规则,在js数据类型很简单,却也不简单这篇笔记中已经简单地解释过了。其实只要记住一条规则:对于==
运算符,如果两个操作数是null
或undefined
,运算结果是true
;否则,不管操作数的类型如何转换,==
运算符最后都是数字的比较。
举几个简单的例子说明下:
1 | null == undefined; // true |
比较运算符
大于>
,大于等于>=
,小于<
,小于等于<=
,用于比较数字的大小或字符在字母表中的排序。要注意的是,在ASCII
中,大写字母排在小写字母前面。
这些比较运算符更偏爱数字的比较,除非两个操作数都是字符串。
对于字符串比较的情况,如果两个字符串的第一个字符是相同的,则会比较第二个字符,以此类推。
这里有一个比较特殊的NaN
,它与任何值做比较都会返回false
。
1 | NaN < 1; // false |
位运算符
位运算符很少用到,但是弄明白它们的运算逻辑是很有必要的。位运算符主要分为与&
、或|
、非~
、异或^
以及左移<<
、带符号右移>>
、无符号右移>>>
等。
位运算符都是二进制的运算,并且是基于32位整数运算。所以十进制,十六进制的操作数都会先转为32位的二进制后再进行运算。这里以0x1234 & 0x00FF = 0x0034
为例说明下流程:
0x123
转为二进制是0000 0000 0000 0000 0001 0010 0011 0100
,0x00FF
转为二进制是0000 0000 0000 0000 0000 0000 0011 0100
。- 进行按位与操作,结果是
0000 0000 0000 0000 0000 0000 0011 0100
,最后转为十六进制就是0x0034
。
移位运算符
在复习到移位运算符这块时,我不由得提出了一个疑问:“javascript中为什么没有无符号左移运算符?”要解答这样一个疑问,首先还是要看看左移和右移分别是怎么运算的。
摘取《计算机组成原理教程》书中的一段描述:
计算机中机器数的字长往往是固定的,当机器数左移n位或右移n位时,必然会使其n位低位或n位高位出现空位。那么,对空出的空位应该添补0还是1呢?这与机器数采用有符号数还是无符号数有关。对无符号数的移位称为逻辑移位,对有符号数的移位称为算术移位。
注意:在javascript中,移位运算符只支持移动0~31位,如果移动的位数超过了31
位,位数会取模MOD 32
。也就是说:
1 | 1 << 32 |
带符号右移>>
对于带符号右移(算术右移)运算而言,第一个操作数是有符号数,它的最高位代表符号位,在移位后的符号位不改变。简单总结就是“低位舍弃,高位补符号位”。
1 | var a = -1; |
如果你自己写几个右移运算表达式做试验,你就会产生一个疑惑,为什么有的正数在带符号右移后却变成了负数,比如下面这个:
1 | 2147483648 >> 31 // -1 |
这是因为32
位的最大带符号正整数是231 - 1,即2147483647
,转换为二进制是0111 1111 1111 1111 1111 1111 1111 1111
。正数的补码与原码相同,2147483648
相当于在此基础上加1
,就得到补码1000 0000 0000 0000 0000 0000 0000 0000
,而这个补码是一个非常特殊的码,它没有对应的原码和补码,代表32
位能表示的带符号数中最小的负数231 - 1,即-2147483648
。而2147483648
在32
位带符号正数中是无法表示的,其值已经溢出了。
计算机只理解二进制,与人类所理解的十进制之间永远存在一个精度问题,需要足够的精度才能更加准确地表示十进制,而计算机的位数永远都是有限的,这就是矛盾存在的地方,所以会出现溢出这种现象。
就好比时钟一般,23
时结束了又从0
时开始。在带符号二进制表示法中,正数和负数首尾相连,形成一个环,在计算机可表示的范围内,溢出的那个数字在某种意义上能在另一个起点找到。
所以,下面的位运算表达式也是等价的:
1 | 2147483649 >> 1 // -1073741824 |
无符号右移>>>
无符号右移也称为逻辑右移。无符号右移的移位过程中,符号位可能会改变。因此移位后,原来的负数可能变成正数。可以简单记忆为“低位舍弃,高位补0”。
1 | -1 >>> 2; // 1073741823 |
左移<<
翻阅《计算机组成原理教程》可以发现,书中有描述到算术左移和逻辑左移。也就是说,左移也分带符号左移和无符号左移。经测试,javascript
中的左移运算符<<
一般不会改变符号位,意味着它是算术左移(其实对比<<
和>>
也能知道,<<
是带符号左移)。
但是左移也要注意溢出的情况,比如:
1 | 1 << 31; // -2147483648 |
那么为什么javascript
中却没有逻辑左移呢?我找了一些资料,比如es5
规范和注解,还有一些javascript
的书籍,都没有找到解释。所以这里也没有一个权威的答案(如果有大佬知道的话,请不吝赐教)。
我个人的想法是,应该是要回到移位运算的本质。
二进制表示的机器数在相对于小数点作n位左移或右移时,其实质就是该数乘以或除以2n(n=1,2, …, n)。
而在左移过程中,如果把符号位都丢了,就失去了乘以2n
的意义了。所以不只是javascript
,其他编程语言如java
等也没有逻辑左移运算符。
最后
不得不说,大学课程真的很重要。如果一直都保持对计算机基础课程的关注,相信理解这些编程语言背后的本质会变得轻松很多。