logo
Published on

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

Authors
  • avatar
    Name
    Eddy Chang
    Twitter

在 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
  1. Type(x)是 String 時,只有當 x 中的字元順序與 y 中完全相同時(長度相同,字元所在位置也相同),回傳 true。其他情況就回傳 false。
  2. Type(x)是 Boolean 時,只有當 x 與 y 是同時為 true 或同時為 false 時,回傳 true。其它情況回傳 false。
  3. 只有當 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 中的相等比較(包含標準的與嚴格的)與關係比較的部份。至於沒提到的,不相等(==)嚴格不相等(!==),或是大於等於(>=)小於等於(<=)只是這些演算規劃的再組合結果而已。

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