讓你徹底搞懂JS中復雜運算符==
大家知道,==
是JavaScript中比較復雜的一個運算符。它的運算規則奇怪,容易讓人犯錯,從而成為JavaScript中“最糟糕的特性”之一。
在仔細閱讀了ECMAScript規范的基礎上,我畫了一張圖,我想通過它你會徹底地搞清楚關於==
的一切。同時,我也試圖通過此文向大家證明==
並不是那么糟糕的東西,它很容易掌握,甚至看起來很合理。
圖1: ==
運算規則的圖形化表示
==
運算規則的精確描述在此:The Abstract Equality Comparison Algorithm。但是,這么復雜的描述,你確定看完后腦子不暈?確定立馬就能拿它指導實踐?
肯定不行,規范畢竟是給JavaScript運行環境的開發人員看的(比如V8引擎的開發人員們),而不是給語言的使用者看的。而上圖正是將規范中復雜的描述翻譯成了更容易看懂的形式。
在詳細介紹圖1中的每個部分前,我們來復習一下JavaScript中關於類型的知識:
- JS中的值有兩種類型:原始類型(Primitive)、對象類型(Object)。
- 原始類型包括:
Undefined
、Null
、Boolean
、Number
和String
等五種。 Undefined
類型和Null
類型的都只有一個值,即undefined
和null
;Boolean
類型有兩個值:true
和false
;Number
類型的值有很多很多;String
類型的值理論上有無數個。- 所有對象都有
valueOf()
和toString()
方法,它們繼承自Object
,當然也可能被子類重寫。
現在考慮表達式:
x == y
其中x
和y
是上述六種類型中某一種類型的值。
當x
和y
的類型相同時,x == y
可以轉化為x === y
,而后者是很簡單的(唯一需要注意的可能是NaN
),所以下面我們只考慮x
和y
的類型不同的情況。
有和無
在圖1中,JavaScript值的六種類型用藍底色的矩形表示。它們首先被分成了兩組:
String
、Number
、Boolean
和Object
(對應左側的大矩形框)Undefined
和Null
(對應右側的矩形框)
分組的依據是什么?我們來看一下,右側的Undefined
和Null
是用來表示不確定、無或者空的,而右側的四種類型都是確定的、有和非空。我們可以這樣說:
左側是一個存在的世界,右側是一個空的世界。
所以,左右兩個世界中的任意值做==
比較的結果都是false
是很合理的。(見圖1中連接兩個矩形的水平線上標的false
)
空和空
JavaScript中的undefined
和null
是另一個經常讓我們崩潰的地方。通常它被認為是一個設計缺陷,這一點我們不去深究。不過我曾聽說,JavaScript的作者最初是這樣想的:
假如你打算把一個變量賦予對象類型的值,但是現在還沒有賦值,那么你可以用
null
表示此時的狀態(證據之一就是typeof null
的結果是object
);相反,假如你打算把一個變量賦予原始類型的值,但是現在還沒有賦值,那么你可以用undefined
表示此時的狀態。
不管這個傳聞是否可信,它們兩者做==
比較的結果是true
是很合理的。(見圖1中右側垂直線上標的true
)
在進行下一步之前,我們先來說一下圖1中的兩個符號:大寫字母N
和P
。這兩個符號並不是PN
結中正和負的意思。而是:
N
表示ToNumber操作,即將操作數轉為數字。它是規范中的抽象操作,但我們可以用JavaScript中的Number()
函數來等價替代。P
表示ToPrimitive操作,即將操作數轉為原始類型的值。它也是規范中的抽象操作,同樣也可以翻譯成等價的JavaScript代碼。不過稍微復雜一些,簡單說來,對於一個對象obj
:
ToPrimitive(obj)
等價於:先計算obj.valueOf()
,如果結果為原始值,則返回此結果;否則,計算obj.toString()
,如果結果是原始值,則返回此結果;否則,拋出異常。
注:此處有個例外,即Date
類型的對象,它會先調用toString()
方法,后調用valueOf()
方法。
在圖1中,標有N
或P
的線表示:當它連接的兩種類型的數據做==
運算時,標有N
或P
的那一邊的操作數要先執行ToNumber
或ToPrimitive
變換。
真與假
從圖1可以看出,當布爾值與其他類型的值作比較時,布爾值會轉化為數字,具體來說
true -> 1 false -> 0
這一點也不需浪費過多口舌。想一下在C語言中,根本沒有布爾類型,通常用來表示邏輯真假的正是整數1
和0
。
字符的序列
在圖1中,我們把String
和Number
類型分成了一組。為什么呢?在六種類型中,String
和Number
都是字符的序列(至少在字面上如此)。字符串是所有合法的字符的序列,而數字可以看成是符合特定條件的字符的序列。所以,數字可以看成字符串的一個子集。
根據圖1,在字符串和數字做==
運算時,需要使用ToNumber
操作,把字符串轉化為數字。假設x
是字符串,y
是數字,那么:
x == y -> Number(x) == y
那么字符串轉化為數字的規則是怎樣的呢?規范中描述得很復雜,但是大致說來,就是把字符串兩邊的空白字符去掉,然后把兩邊的引號去掉,看它能否組成一個合法的數字。如果是,轉化結果就是這個數字;否則,結果是NaN
。例如:
Number('123') // 結果123 Number('1.2e3') // 結果1200 Number('123abc') // 結果NaN Number('\r\n\t123\v\f') // 結果123
當然也有例外,比如空白字符串轉化為數字的結果是0
。即
Number('') // 結果0 Number('\r\n\t \v\f') // 結果0
單純與復雜
原始類型是一種單純的類型,它們直接了當、容易理解。然而缺點是表達能力有限,難以擴展,所以就有了對象。對象是屬性的集合,而屬性本身又可以是對象。所以對象可以被構造得任意復雜,足以表示各種各樣的事物。
但是,有時候事情復雜了也不是好事。比如一篇冗長的論文,並不是每個人都有時間、有耐心或有必要從頭到尾讀一遍,通常只了解其中心思想就夠了。於是論文就有了關鍵字、概述。JavaScript中的對象也一樣,我們需要有一種手段了解它的主要特征,於是對象就有了toString()
和valueOf()
方法。
toString()
方法用來得到對象的一段文字描述;而valueOf()
方法用來得到對象的特征值。
當然,這只是我自己的理解。顧名思義,toString()
方法傾向於返回一個字符串。那么valueOf()
方法呢?根據規范中的描述,它傾向於返回一個數字——盡管內置類型中,valueOf()
方法返回數字的只有Number
和Date
。
根據圖1,當一個對象與一個非對象比較時,需要將對象轉化為原始類型(雖然與布爾類型比較時,需要先將布爾類型變成數字類型,但是接下來還是要將對象類型變成原始類型)。這也是合理的,畢竟==
是不嚴格的相等比較,我們只需要取出對象的主要特征來參與運算,次要特征放在一邊就行了。
萬物皆數
我們回過頭來看一下圖1。里面標有N或P的那幾條連線是沒有方向的。假如我們在這些線上標上箭頭,使得連線從標有N
或P
的那一端指向另一端,那么會得到(不考慮undefined
和null
):
圖2: ==
運算過程中類型轉化的趨勢
發現什么了嗎?對,在運算過程中,所有類型的值都有一種向數字類型轉化的趨勢。畢竟曾經有名言曰:
萬物皆數。
舉個栗子
前面廢話太多了,這里還是舉個例子,來證明圖1確實是方便有效可以指導實踐的。
例,計算下面表達式的值:
[''] == false
首先,兩個操作數分別是對象類型、布爾類型。根據圖1,需要將布爾類型轉為數字類型,而false
轉為數字的結果是0
,所以表達式變為:
[''] == 0
兩個操作數變成了對象類型、數字類型。根據圖1,需要將對象類型轉為原始類型:
- 首先調用
[].valueOf()
,由於數組的valueOf()
方法返回自身,所以結果不是原始類型,繼續調用[].toString()
。 - 對於數組來說,
toString()
方法的算法,是將每個元素都轉為字符串類型,然后用逗號,
依次連接起來,所以最終結果是空字符串''
,它是一個原始類型的值。
此時,表達式變為:
'' == 0
兩個操作數變成了字符串類型、數字類型。根據圖1,需要將字符串類型轉為數字類型,前面說了空字符串變成數字是0
。於是表達式變為:
0 == 0
到此為止,兩個操作數的類型終於相同了,結果明顯是true
。
從這個例子可以看出,要想掌握==
運算的規則,除了牢記圖1外,還需要記住那些內置對象的toString()
和valueOf()
方法的規則。包括Object
、Array
、Date
、Number
、String
、Boolean
等,幸好這沒有什么難度。
再次變形
其實,圖一還不夠完美。為什么呢?因為對象與字符串/數字比較時都由對象來轉型,但是與同樣是原始類型的布爾類型比較時卻需要布爾類型轉型。實際上,只要稍稍分析一下,全部讓對象來轉為原始類型也是等價的。所以我們得到了最終的更加完美的圖形:
圖3: 更完美的==
運算規則的圖形化表示
有一個地方可能讓你疑惑:為什么Boolean
與String
之間標了兩個N
?雖然按照規則應該是由Boolean
轉為數字,但是下一步String
就要轉為數字了,所以干脆不如兩邊同時轉成數字。
總結一下
前面說得很亂,根據我們得到的最終的圖3,我們總結一下==運算的規則:
undefined == null
,結果是true
。且它倆與所有其他值比較的結果都是false
。String == Boolean
,需要兩個操作數同時轉為Number
。String/Boolean == Number
,需要String/Boolean
轉為Number
。Object == Primitive
,需要Object
轉為Primitive
(具體通過valueOf()
和toString()
方法)。
瞧見沒有,一共只有4條規則!是不是很清晰、很簡單。
最后,我需要@一下Belleve大神,為什么呢?因為整篇文章的思考,都是在看到他在《Javascript 中 ==
和 ===
區別是什么?》中的回答后做出的。當時他貼了一張圖:
我看后覺得太復雜了,於是想能不能用一種更簡單的方式來描述一下==
運算,使大家更清晰更容易掌握。於是就有了此文,當然我不知道自己成功了沒有。