千万别小看这些运算符背后的逻辑

微信扫一扫,分享到朋友圈

千万别小看这些运算符背后的逻辑

前言

最近回顾 javascript 的一些基础知识点时,引起的思考确实颠覆了我之前的一些认知。我清楚地记得曾多次在网上看到一些奇奇怪怪的表达式,它们的运算结果着实让人懵逼。就比如我在 js数据类型很简单,却也不简单 这一篇笔记中提到的 [] == ![] 这样一个表达式,它的运算结果是 true 。如果你不细致地去研究它背后的运算逻辑,你只会惊呼”这是什么鬼“?相反,当你静下心来看清楚它的运算逻辑后,你会感叹“妙哉妙哉”!没错,本文的主角就是这些容易让人小觑的运算符

加法运算符+

首先说的是加法运算符 + ,这是一个很容易被人忽视的运算符。我们知道, + 可以用来做数字运算,也可以用作字符串拼接,但是还有一些细节可能是大家不知道的。如果 + 运算符的两个操作数类型不一致,或者说两个操作数既不是字符串也不是数字,那么它的运算规则是什么?

先举几个例子,你可以先思考下这些运算结果分别是什么。

var a = 1 + "1";
var b = 1 + {};
var c = 1 + [];
var d = 1 + true;
var e = { name: '飞白' } + [1, 2];
var f = null + undefined;
var g = true + null;

其实规则很简单,我们只要简单地列举出数据类型的可能性,就几乎得到了完整的答案。

  1. 如果操作数都是数字,进行数字的加法运算。
  2. 如果操作数都是字符串,进行字符串的拼接。
  3. 如果操作数是对象,会转换为原始值(一般是先调用 valueOf() ,日期对象比较特殊,会调用 toString() ),得到的原始值不再被强制转换为数字或字符串。在这种约束下,对象转为原始值基本都是字符串(如果你没有重写 valuOf() 或者 toString() 方法),根据下面的第四点,会执行字符串拼接操作。
  4. 如果其中一个操作数是字符串,另一个操作数也会被转为字符串, + 运算符执行字符串拼接操作。
  5. 如果两个操作数都不是字符串或对象,则会进行算术加法运算(非数字的操作数会被强制转为数字)。

所以,不难得出上面列举的表达式的运算结果。

var a = 1 + "1"; // "11"
var b = 1 + {}; // "1[object Object]"
var c = 1 + []; // "1"
var d = 1 + true; // 2
var e = { name: '飞白' } + [1, 2]; // "[object Object]1,2"
var f = null + undefined; // NaN
var g = true + null; // 1

要记住这些规则并不简单,一个记忆技巧是: + 运算符偏爱字符串拼接操作。

相等运算符==

这个运算符的运算规则,在 js数据类型很简单,却也不简单 这篇笔记中已经简单地解释过了。其实只要记住一条规则:对于 == 运算符,如果两个操作数是 nullundefined ,运算结果是 true ;否则,不管操作数的类型如何转换, == 运算符最后都是数字的比较。

举几个简单的例子说明下:

null == undefined; // true
[1] == 1; // true
1 == true; // true
1 == "1" // true
new Date(2020, 0, 1, 0, 0, 0) == 1577808000000 // false

比较运算符

大于 > ,大于等于 >= ,小于 < ,小于等于 <= ,用于比较数字的大小或字符在字母表中的排序。要注意的是,在 ASCII 中,大写字母排在小写字母前面。

这些比较运算符更偏爱数字的比较,除非两个操作数都是字符串。

对于字符串比较的情况,如果两个字符串的第一个字符是相同的,则会比较第二个字符,以此类推。

这里有一个比较特殊的 NaN ,它与任何值做比较都会返回 false

NaN < 1; // false
NaN > 1; // false

位运算符

位运算符很少用到,但是弄明白它们的运算逻辑是很有必要的。位运算符主要分为与 & 、或 | 、非 ~ 、异或 ^ 以及左移 << 、带符号右移 >> 、无符号右移 >>> 等。

位运算符都是二进制的运算,并且是基于32位整数运算。所以十进制,十六进制的操作数都会先转为32位的二进制后再进行运算。这里以 0x1234 & 0x00FF = 0x0034 为例说明下流程:

  1. 0x123 转为二进制是 0000 0000 0000 0000 0001 0010 0011 01000x00FF 转为二进制是 0000 0000 0000 0000 0000 0000 0011 0100
  2. 进行按位与操作,结果是 0000 0000 0000 0000 0000 0000 0011 0100 ,最后转为十六进制就是 0x0034

移位运算符

在复习到移位运算符这块时,我不由得提出了一个疑问:“javascript中为什么没有无符号左移运算符?”要解答这样一个疑问,首先还是要看看左移和右移分别是怎么运算的。

摘取《计算机组成原理教程》书中的一段描述:

计算机中机器数的字长往往是固定的,当机器数左移n位或右移n位时,必然会使其n位低位或n位高位出现空位。那么,对空出的空位应该添补0还是1呢?这与机器数采用有符号数还是无符号数有关。对无符号数的移位称为逻辑移位,对有符号数的移位称为算术移位。

注意:在javascript中,移位运算符只支持移动0~31位,如果移动的位数超过了 31 位,位数会取模 MOD 32 。也就是说:

1 << 32
// 等价于
1 << 0

带符号右移>>

对于带符号右移(算术右移)运算而言,如果第一个操作数是有符号数,那么它的最高位代表符号位,在移位后的符号位不改变。简单总结就是“低位舍弃,高位补0,符号位保留”。

var a = -1;
a >> 2; // -1
// 如果用负数的原码形式进行算术右移,高位补0,符号位置1

如果你自己写几个右移运算表达式做试验,你就会产生一个疑惑,为什么有的正数在带符号右移后却变成了负数,比如下面这个:

2147483648 >> 31 // -1

这是因为 32 位的最大带符号正整数是2 31 – 1,即 2147483647 ,转换为二进制是 0111 1111 1111 1111 1111 1111 1111 1111 。正数的补码与原码相同, 2147483648 相当于在此基础上加 1 ,就得到补码 1000 0000 0000 0000 0000 0000 0000 0000 ,而这个补码是一个非常特殊的码,它没有对应的原码和补码,代表 32 位能表示的带符号数中最小的负数2 31 – 1,即 -2147483648 。而 214748364832 位带符号正数中是无法表示的,其值已经溢出了。

计算机只理解二进制,与人类所理解的十进制之间永远存在一个精度问题,需要足够的精度才能更加准确地表示十进制,而计算机的位数永远都是有限的,这就是矛盾存在的地方,所以会出现溢出这种现象。

就好比时钟一般, 23 时结束了又从 0 时开始。在带符号二进制表示法中,正数和负数首尾相连,形成一个环,在计算机可表示的范围内,溢出的那个数字在某种意义上能在另一个起点找到。

所以,下面的位运算表达式也是等价的:

2147483649 >> 1 // -1073741824
-2147483647 >> 1 // 可以理解为:2147483649溢出的值为2,所以在位运算中,等价于第二小的负数-2147483647

无符号右移>>>

无符号右移也称为逻辑右移。无符号右移的移位过程中,符号位可能会改变。因此移位后,原来的负数可能变成正数。可以简单记忆为“低位舍弃,高位补0”。

-1 >>> 2; // 1073741823
// 1000 0000 0000 0000 0000 0000 0000 0001 右移两位变成 0010 0000 0000 0000 0000 0000 0000 0000
// 也就是2的30次方减去1,等于1073741823

左移<<

翻阅《计算机组成原理教程》可以发现,书中有描述到算术左移和逻辑左移。也就是说,左移也分带符号左移和无符号左移。经测试, javascript 中的左移运算符 << 一般不会改变符号位,意味着它是算术左移(其实对比 <<>> 也能知道, << 是带符号左移)。

但是左移也要注意溢出的情况,比如:

1 << 31; // -2147483648

那么为什么 javascript 中却没有逻辑左移呢?我找了一些资料,比如 es5 规范和注解,还有一些 javascript 的书籍,都没有找到解释。所以这里也没有一个权威的答案(如果有大佬知道的话,请不吝赐教)。

我个人的想法是,应该是要回到移位运算的本质。

二进制表示的机器数在相对于小数点作n位左移或右移时,其实质就是该数乘以或除以2n(n=1,2, …, n)。

而在左移过程中,如果把符号位都丢了,就失去了乘以 2n 的意义了。所以不只是 javascript ,其他编程语言如 java 等也没有逻辑左移运算符。

最后

不得不说,大学课程真的很重要。如果一直都保持对计算机基础课程的关注,相信理解这些编程语言背后的本质会变得轻松很多。

跨城实践中,腾讯如何应用 Apache Pulsar

上一篇

发现数据结构之美-栈

下一篇

你也可能喜欢

千万别小看这些运算符背后的逻辑

长按储存图像,分享给朋友