- Published on
JS中的關係比較與相等比較運算
- Authors
- Name
- Eddy Chang
在 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 是個獨立的資料類型。
- Type(x)與 Type(y)不同,回傳 false
- Type(x)是 Undefined,回傳 true(當然此時 Type(y)也是 Undefined)
- Type(x)是 Null,回傳 true(當然此時 Type(y)也是 Null)
- 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
- Type(x)是 String 時,只有當 x 中的字元順序與 y 中完全相同時(長度相同,字元所在位置也相同),回傳 true。其他情況就回傳 false。
- Type(x)是 Boolean 時,只有當 x 與 y 是同時為 true 或同時為 false 時,回傳 true。其它情況回傳 false。
- 只有當 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,會按照下面的步驟進行比較,如果有回傳時就停止之後的步驟:
- Type(x)與 Type(y)相同時,進行嚴格相等比較
- x 是 undefined,而 y 是 null 時,回傳 true
- x 是 null,而 y 是 undefined 時,回傳 true
- Type(x)是 Number 而 Type(y)是 String 時,進行
x == ToNumber(y)
比較 - Type(x)是 String 而 Type(y)是 Number 時,進行
ToNumber(x) == y
比較 - Type(x)是 Boolean 時,進行
ToNumber(x) == y
- Type(y)是 Boolean 時,進行
x == ToNumber(y)
- Type(x)是 Number 或 String 其中一種,而 Type(y)是個 Object 時,進行
x == ToPrimitive(y)
比較 - Type(x)是個 Object,而 Type(y)是 Number 或 String 其中一種時,進行
ToPrimitive(x) == y
比較 - 其他情況,回傳 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 所定義的字串相等或校對順序。
註:
+∞
相當於全域屬性Infinity
或Number.POSITIVE_INFINITY
,−∞
相當於全域屬性-Infinity
或Number.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 中的相等比較(包含標準的與嚴格的)與關係比較的部份。至於沒提到的,不相等(==)
與嚴格不相等(!==)
,或是大於等於(>=)
或小於等於(<=)
只是這些演算規劃的再組合結果而已。
標準的值相等比較(==)
,是一種有不經意的副作用的運算,不管如何,開發者必定要儘量避免,比較前可以自行轉換類型的方式,再作嚴格的相等比較,本章也有說明為何要避免使用它的理由。