通過一張簡單的圖,讓你徹底地、永久地搞懂JS的==運算
大家知道,==是JavaScript中比較複雜的一個運算子。它的運算規則奇怪,容易讓人犯錯,從而成為JavaScript中“最糟糕的特性”之一。
在仔細閱讀了ECMAScript規範的基礎上,我畫了一張圖,我想通過它你會徹底地搞清楚關於==的一切。同時,我也試圖通過此文向大家證明==並不是那麼糟糕的東西,它很容易掌握,甚至看起來很合理。
先上圖:
圖1 ==運算規則的圖形化表示
==運算規則的精確描述在此:The Abstract Equality Comparison Algorithm。但是,這麼複雜的描述,你確定看完後腦子不暈?確定立馬就能拿它指導實踐?
肯定不行,規範畢竟是給JavaScript執行環境的開發人員看的(比如V8引擎的開發人員們),而不是給語言的使用者看的。而上圖正是將規範中複雜的描述翻譯成了更容易看懂的形式。
在詳細介紹圖1中的每個部分前,我們來複習一下JS中關於型別的知識:
-
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操作,即將運算元轉為數字。它是規範中的抽象操作,但我們可以用JS中的Number()函式來等價替代。
-
P表示ToPrimitive操作,即將運算元轉為原始型別的值。它也是規範中的抽象操作,同樣也可以翻譯成等價的JS程式碼。不過稍微複雜一些,簡單說來,對於一個物件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大神,為什麼呢?因為整篇文章的思考,都是在看到他在https://www.zhihu.com/question/31442029中的回答後做出的。當時他貼了一張圖:
我看後覺得太複雜了,於是想能不能用一種更簡單的方式來描述一下==運算,使大家更清晰更容易掌握。於是就有了此文,當然我不知道自己成功了沒有。
OK,結束了。文章中的謬誤,請不吝指出。
PS:最後,把圖改了一下,僅供娛樂 : )
作者:蘇雲
相關文章
- 兩張圖徹底搞懂MyBatis的Mapper原理!MyBatisAPP
- 一張圖徹底搞懂Spring迴圈依賴Spring
- 看完讓你徹底搞懂Websocket原理Web
- 幫你徹底搞懂JS中的prototype、__proto__與constructor(圖解)JSStruct圖解
- Rize - 一個可以讓你簡單、優雅地使用 puppeteer 的 Node.js 庫Node.js
- 幫你徹底搞懂JS中的prototype、__proto__與constructor(圖解)(轉)JSStruct圖解
- 一張圖徹底搞定 explainAI
- 【計算機內功心法】六:10張圖讓你徹底理解回撥函式計算機函式
- 高頻面試題:一張圖徹底搞懂Spring迴圈依賴面試題Spring
- 一篇文章帶你徹底搞懂join的用法
- 這篇文章讓你徹底搞懂ES6中的Class(全面解析)
- 一張圖徹底理解Javascript原型鏈JavaScript原型
- 徹底搞懂Scrapy的中介軟體(一)
- SpringCloud | 通過電商業務場景讓你徹底明白SpringCloud核心元件的底層原理SpringGCCloud元件
- 怎麼自己製作地圖,簡單的地圖繪製軟體地圖
- 天啦嚕!僅僅5張圖,徹底搞懂Python中的深淺拷貝Python
- 徹底搞懂徹底搞懂事件驅動模型 - Reactor事件模型React
- 一張新型肺炎地區分佈地圖是怎麼製作的?地圖
- 室內地圖導航製作,簡單的地圖繪製軟體地圖
- 徹底搞懂Python中的類Python
- 徹底搞懂 Kubernetes 中的 Events
- 一文徹底搞懂BERT
- 12張圖帶你徹底理解分散式事務!!分散式
- 【愣錘筆記】一篇小短文讓你徹底搞懂this、call、apply和bind筆記APP
- 百度地圖,您所使用的地圖JS API版本過低,解決方法地圖JSAPI
- 徹底說透簡單工廠那些你沒有關注過的細節
- 一張圖徹底KO原型鏈(prototype,__proto__)原型
- 51 張圖助你徹底掌握 HTTP!HTTP
- 徹底搞懂JavaScript中的繼承JavaScript繼承
- 徹底搞懂HTTPS的加密機制HTTP加密
- 我畫了 40 張圖就是為了讓你搞懂計算機網路層計算機網路
- 徹底搞懂https原理HTTP
- 如何讓Designer更好地運算元據庫物件物件
- 一張圖帶你搞懂Node事件迴圈事件
- 一文徹底搞懂 Javascript 的 this(含 ES6+)JavaScript
- 7000字+24張圖帶你徹底弄懂執行緒池執行緒
- 仿鏈家地圖找房的簡單實現地圖
- 徹底搞懂Scrapy的中介軟體(二)
- 徹底搞懂Scrapy的中介軟體(三)