前言
最近回顧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;
其實規則很簡單,我們只要簡單地列舉出數據類型的可能性,就幾乎得到了完整的答案。
- 如果操作數都是數字,進行數字的加法運算。
- 如果操作數都是字符串,進行字符串的拼接。
- 如果操作數是對象,會轉換爲原始值(一般是先調用
valueOf()
,日期對象比較特殊,會調用toString()
),得到的原始值不再被強制轉換爲數字或字符串。在這種約束下,對象轉爲原始值基本都是字符串(如果你沒有重寫valuOf()
或者toString()
方法),根據下面的第四點,會執行字符串拼接操作。 - 如果其中一個操作數是字符串,另一個操作數也會被轉爲字符串,
+
運算符執行字符串拼接操作。 - 如果兩個操作數都不是字符串或對象,則會進行算術加法運算(非數字的操作數會被強制轉爲數字)。
所以,不難得出上面列舉的表達式的運算結果。
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數據類型很簡單,卻也不簡單這篇筆記中已經簡單地解釋過了。其實只要記住一條規則:對於==
運算符,如果兩個操作數是null
或undefined
,運算結果是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
爲例說明下流程:
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 << 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
。而2147483648
在32
位帶符號正數中是無法表示的,其值已經溢出了。
計算機只理解二進制,與人類所理解的十進制之間永遠存在一個精度問題,需要足夠的精度才能更加準確地表示十進制,而計算機的位數永遠都是有限的,這就是矛盾存在的地方,所以會出現溢出這種現象。
就好比時鐘一般,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
等也沒有邏輯左移運算符。
最後
不得不說,大學課程真的很重要。如果一直都保持對計算機基礎課程的關注,相信理解這些編程語言背後的本質會變得輕鬆很多。