千萬別小看這些運算符背後的邏輯

前言

最近回顧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

帶符號右移>>

對於帶符號右移(算術右移)運算而言,第一個操作數是有符號數,它的最高位代表符號位,在移位後的符號位不改變。簡單總結就是“低位捨棄,高位補符號位”。

var a = -1;
a >> 2; // -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。而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等也沒有邏輯左移運算符。

最後

不得不說,大學課程真的很重要。如果一直都保持對計算機基礎課程的關注,相信理解這些編程語言背後的本質會變得輕鬆很多。

歡迎交流

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章