JS中的關係比較與相等比較運算


在JS中的關係比較(Relational Comparison)運算,指的是像x < y這種大小值的關係比較。

而相等比較,可區分為標準相等(standard equality)比較x == y與嚴格相等(strict equality)比較x === y兩大種類。嚴格相等比較會比較左邊與右邊運算元的資料類型,值相等比較則只看值,簡單的來說是這樣解釋沒錯。

ToPrimitive運算的詳細說明可參考: JS中的{} + {}與{} + []的結果是什麼?

不過,這兩種比較實際上依內部設計來說,並不是那麼簡單。當然,在一般的使用情況是不需要考量那麼多,本文的說明會涉及許多JS內部設計的部份,對於這兩種比較來作比較徹底的理解,主要的參考資料是ECMAScript的標準文件。

嚴格相等比較(嚴格相等比較演算)

嚴格相等比較的演算規則先理解,主要是因為在標準相等比較(只比較值不比較資料類型)時,它在演算時的某些情況下會跳到嚴格相等比較的規則來。

嚴格相等比較的演算規則很容易理解,按照以下的步驟進行比較,出自ecma-262 11.9.6:


以下假設為比較 x === y的情況,Type(x)指的是x的資料類型,Type(y)指的是y的類型,最終回傳值只有true或false,會按照下面的步驟進行比較,如果有回傳時就停止之後的步驟:

註: Type(x)在ECMAScript的標準中指的並不是用typeof回傳出來的結果,而是標準內部給定的各種資料類型,共有Undefined, Null, Boolean, String, Number 與 Object。例如typeof null的結果是"object",但ECMAScript會認為Null是個獨立的資料類型。

  1. Type(x)與Type(y)不同,回傳false
  2. Type(x)是Undefined,回傳true(當然此時Type(y)也是Undefined)
  3. Type(x)是Null,回傳true(當然此時Type(y)也是Null)
  4. Type(x)是Number時
    • a. x是NaN,回傳false
    • b. y是NaN,回傳false
    • c. x與y是同樣的數字,回傳true
    • d. x是+0,y是-0,回傳true
    • e. x是-0,y是+0,回傳true
    • f. 其他情況,回傳false
  5. Type(x)是String時,只有當x中的字元順序與y中完全相同時(長度相同,字元所在位置也相同),回傳true。其他情況就回傳false。
  6. Type(x)是Boolean時,只有當x與y是同時為true或同時為false時,回傳true。其它情況回傳false。
  7. 只有當x與y同時參照到同一物件時,回傳true。其它情況回傳false。

備註: 這個演算與the SameValue Algorithm (9.12)不同之處在於,對於有號的0與NaN處理方式不同。


註: 同值演算(the SameValue Algorithm)是標準中的另一個內部演算法,只會用在很特別的地方,可以先略過不看。

從上述的嚴格相等比較中,可以很清楚的看到數字、字串、布林與null、undefined或物件是如何比較的。

標準相等比較(抽象相等比較演算)

標準相等比較的演算規則按照以下的步驟進行比較,出自ecma-262 11.9.3:


以下假設為比較 x == y的情況,Type(x)指的是x的資料類型,Type(y)指的是y的類型,最終回傳值只有true或false,會按照下面的步驟進行比較,如果有回傳時就停止之後的步驟:

  1. Type(x)與Type(y)相同時,進行嚴格相等比較
  2. x是undefined,而y是null時,回傳true
  3. x是null,而y是undefined時,回傳true
  4. Type(x)是Number而Type(y)是String時,進行x == ToNumber(y)比較
  5. Type(x)是String而Type(y)是Number時,進行ToNumber(x) == y比較
  6. Type(x)是Boolean時,進行ToNumber(x) == y
  7. Type(y)是Boolean時,進行x == ToNumber(y)
  8. Type(x)是Number或String其中一種,而Type(y)是個Object時,進行x == ToPrimitive(y)比較
  9. Type(x)是個Object,而Type(y)是Number或String其中一種時,進行ToPrimitive(x) == y比較
  10. 其他情況,回傳false

備註1: 以下的是三種強制轉換的標準比較情況:

  • 字串比較: "" + a == "" + b.
  • 數字比較: +a == +b.
  • 布林比較: !a == !b

備註2: 標準相等比較有以下的不變式(invariants):

  • A != B 相當於 !(A == B)
  • A == B 相當於 B == A

備註3: 相等比較運算不一定總是可以轉變(transitive),例如:

  • new String("a") == "a" 與 "a" == new String("a") 的結果都是true
  • new String("a") == new String("a") 結果是false.

備註4: 字串比較使用的是簡單的字元測試。並非使用複雜的、語義導向的字元定義或是Unicode所定義的字串相等或校對順序。

註: 上述的ToNumber與ToPrimitive都是標準內部運算時使用的方法,並不是讓開發者使用的。


由標準相等比較的演算得知,它的運算是以"數字為最優先",任何其它的類型如果與數字作相等比較,必定要先強制轉為數字再比較。但這是一個相當具有隱藏作用的運算,在一般實作時,會很容易造成誤解,例如以下的範例:

> 0 == []
true

> '' == []
true

上面這是因為空陣列[],進行ToPrimitive運算後,得到的是空字串,所以作值相等比較,相當於空字串在進行比較。

> '[object Object]' == {}
true

> NaN == {}
false

上面的空物件字面量,進行ToPrimitive運算後,得到的是'[object Object]'字串,這個值會如果與數字類型的NaN比較,會跳到同類型相等的嚴格相等比較中,NaN不論與任何數字作相等比較,一定是回傳false。

> 1 == new Number(1)
true

> 1 === new Number(1)
false

> 1 === Number(1)
true

上面說明了,包裝物件在JS中的內部設計中,標準的值相等比較是相同的,但嚴格相等比較是不同的值,包裝物件仍然是個物件,只是裡面的valueOf方法是回傳這個物件裡面帶的原始資料類型值,經過ToPrimitive方法運算後,會回傳原始資料的值。Number()函式呼叫只是轉數字類型用的函式,這個用法經常會與包裝物件的用法混在一起。

這個小節的結論是,在JS中沒有必要的情況下,使用嚴格的相等比較為最佳值比較方式,標準的相等比較容易產生不經意的副作用,有的時候你可能會得到不預期的結果。

關係比較(抽象關係比較演算)

關係比較的演算規則主要是按照以下的步驟進行比較,出自ecma-262 11.8.5:


以下假設為比較 x < y的情況,因為在標準中的抽象關係比較演算的說明比較複雜,有涉及布林標記的以左方優先或右方優先,而且最終回傳值有true、false與undefined,實際上最終不會有undefined值出現,即是得到false而已,以下為只考慮左方優先(LeftFirst)的簡化過的步驟。會按照下面的步驟進行比較,如果有回傳時就停止之後的步驟:

  • (1. & 2.) x經過ToPrimitive(x, hint Number)運算為px值,y經過ToPrimitive(y, hint Number)運算為py值
  • (3.) 如果Type(px)與Type(py)不同時為String時
    • a.b. px作ToNumber(px)運算,得到nx值,與py作ToNumber(py)值,得到ny值
    • c.d. nx或ny中有其一為NaN時,回傳undefined
    • e. nx與ny是同樣的Number值,回傳false
    • f. nx是+0,而且ny是−0,回傳false
    • g. nx是−0,而且ny是+0,回傳false.
    • h. nx是+∞,回傳false
    • i. ny是+∞,回傳true
    • j. ny是−∞,回傳false
    • k. nx是−∞,回傳true
    • l. 如果在數學上的值,nx小於ny,而且nx與ny是有限值(finite),而且不同時為0時,回傳true。否則回傳false。
  • (4.) 如果Type(px)與Type(py)同時為String時
    • a. 如果py是px的前綴(prefix)時,回傳false (前綴代表px字串中是由py字串組成的,py只是px的子字串的情況)
    • b. 如果px是py的前綴(prefix)時,回傳true
    • c.d.e.f 以字串中的按順序的字元,用字元的編碼整數的大小來比較。k是可得到的一個最小非負整數,在px與py中的k位置有不同的字元(從左邊算過來)。在px中某個位置k的字元編碼整數為m,在py某個位置k的字元編輯為n,如果m < n,則回傳true,否則回傳false

備註2: 字串比較使用的是簡單的詞典順序測試。並非使用複雜的、語義導向的字元定義或是Unicode所定義的字串相等或校對順序。

註: +∞相當於全域屬性InfinityNumber.POSITIVE_INFINITY−∞相當於全域屬性-InfinityNumber.NEGATIVE_INFINITY


關係比較基本上要區分為數字類型與字串類型,但依然是以"數字"為最優先的比較,只要有其他類型與數字相比較,一定會先被強制轉換為數字。但在這之前,需要先用ToPrimitive而且是hint為數字來轉換為原始資料類型。

以下為一些與物件、陣列、Date物件的關係比較範例:

> 1 < (new Date())
true

> 1 > (new Date())
false

> [] < 1
true

> [] > 1
false

> ({}) < 1
false

> ({}) > 1
false

雖然在標準中的抽象關係比較演算中,有存在一種回傳值undefined,但在真實的情況並沒有這種回傳值,相當不論怎麼比較都是得到false的值。上面的例子中,空物件({})的ToPrimitive運算得出的是'[object Object]'字串值,經過ToNumber運算會得到NaN數字類型的值,這個值不論與數字1作大於小於的關係運算,都是false。

Date()物件因為ToPrimitive運算的hint為數字,所以也是會作轉換為數字類型的值為優先(也就是呼叫valueOf為優先),所以並不是正常情況的以輸出字串為優先(也就是呼叫toString方法為優先)的預設情況。

以下為一些字串關係比較的範例:

> 'a' > ''
true

> 'a' < ''
false

> 'a' > []
true

> 'a' < []
false

> 'a' > ({})
true

> 'a' < ({})
false

字串與空字串相比,都是套用前綴(prefix)的規則步驟,因為空字串算是所有字串的前綴(組成的子字串之一),所以必然地所有有值的字串值一定是大於空字串。

空陣列經過ToPrimitive運算出來的是空字串,所以與空字串相比較的結果相同。

空物件經過ToPrimitive運算出來的是'[object Object]'字串值,以'a'.charCodeAt(0)計算出的值是字元編碼是97數字,而'['.charCodeAt(0)則是91數字,所以'a' > ({})會是得到true。

如果開始混用數字與字串比較,可能是有陷阱的比較例子:

> '11' > '3'
false

> '11' > 3
true

> 'one' < 3
false

> 'one' > 3
false

'11'與'3'相比較,其實都是字串比較,要依照可比較的字元位置來比較,也就是'1'與'3'字元的比較,它們的字元編碼數字分別是49與51,所以'1' < '3',這裡的運算的結果必然是回傳false。

'11'與3數字比較,是會強制都轉為數字來比較,'11'會轉為11數字值,所以大於3。

'one'這個字串轉為數字後,是NaN這個數字值,NaN與任何數字比較,既不大於也不小於,不論作大於或小於,都是回傳false。(實際上在標準中它這種回傳值叫undefined)

字串與數字之外其他的原始資料類型的比較,只要記得原則就是強制轉為數字來比較就是了,以下為例子:

> true > null
true

> false > undefined
false

簡單地說明在ToNumber運算時,這些其他的原始資料類型值的轉換結果如下:

  • Undefined -> NaN
  • Null -> +0
  • Boolean -> (true -> 1, false -> 0)

註: JS認為+0與-0是完全相同的值,在嚴格相等比較中是相等的。

註: 字串比較實際上是拆為字元在詞典表中的編輯整數值來比較,對於非英語系的語言,JS另外有提供String.prototype.localeCompare的方法來進行本地語言的比較工作。

總結

本章延伸了之前的加法運算中的ToPrimitive運算的部份,較為仔細的來研究JS中的相等比較(包含標準的與嚴格的)與關係比較的部份。至於沒提到的,不相等(==)與嚴格不相等(!==),或是大於等於(>=)或小於等於(<=)只是這些演算規劃的再組合結果而已。

標準的值相等比較(==),是一種有不經意的副作用的運算,不管如何,開發者必定要儘量避免,比較前可以自行轉換類型的方式,再作嚴格的相等比較,本章也有說明為何要避免使用它的理由。