程式語言的相等性,永遠有說不完的故事,然而,也是開發者經常容易忽略的一環,在JavaScript中,相等性似乎是更少被提及的話題,一律使用===就能解決問題嗎?

就像在ECMAScript中,為了讓相等性的定義明確,而規範了SameValue與SameValueZero,與相等性有關的API,但是,你知道它們各自是使用哪個定義嗎?

抽象相等、嚴格相等

在JavaScript中,兩個值若要比較彼此是否相等,開發者熟悉的就是==與===運算,JavaScript是個十足的弱型別語言,許多情況下會自動型態轉換,這就使得==在相等判斷時,容易出現許多開發者難以掌握的狀況。

實際上,這個相等比較,被稱為抽象相等比較(Abstract Equality Comparison),而在ECMAScript 3中,有著長達22個步驟的雜亂判斷順序。

長久的經驗累積之下,JavaScript開發者通常都知道,在多數情況下,應該使用===,以嚴格的方式來比較兩值是否相等。

簡單來說,許多抽象相等比較,因自動型態轉換而結果為true的情況(例如null與undefined),在===中,會是false。而這在ECMAScript 3中,稱為嚴格相等比較(Strict Equality Comparison),規範中有13個步驟的判斷順序。

有趣的是,規格書中相等性比較的定義變化。在ECMAScript 5.1中,抽象相等比較看似只有10個步驟,實際上,是將ECMAScript 3的前13個步驟做個整理,使之納為第1個步驟的各個子步驟,嚴格相等比較也是類似地做了重新排版的動作,主要步驟縮減為7個。

仔細觀察ECMAScript 5.1,抽象相等比較在==兩邊的型態相同時,接下來的判斷,其實與嚴格相等比較是重疊的,因此在ECMAScript 6,抽象相等比較在==兩邊型態相同時,就直接告訴你看嚴格相等比較中定義的步驟了。

現在,問題來了,你知道Array的indexOf、lastIndexOf在比較值的時候,或者是switch的case比對之際,是採用哪一種類型的比較嗎?答案是嚴格相等比較。

SameValueZero演算

如果仔細看看嚴格相等比較的規範,會發現只要出現NaN,就會是false,即使是NaN === NaN也會是false,能判斷某值是不是NaN的函式為isNaN。不過,isNaN存在自動轉型問題而不怎麼可靠,例如isNaN('c')會是true,isNaN比較像是isNotaNumber的行為。而在ECMAScript 6中,有個Number.isNaN改善了這點,傳入NaN的話才會是true。

如果陣列包含NaN做為元素,那麼indexOf(NaN)找到得索引,或者是傳回-1呢?

由於採用嚴格相等比較,結果是會傳回-1的,同樣地,switch的case若試圖比對NaN,該比對案例是不可能被執行的。

在ECMAScript 6中,規範了SameValueZero演算,它與嚴格相等演算的差異在於,一開始採用了ReturnIfAbrupt(抽象相等演算一開始也是),簡單來說,比對的若不是個正常值就中止執行,另外,如果比較的兩個值是NaN,結果會是true,其他比對步驟與===相同。

ECMAScript 6所新增的Set與Map,就是採用SameValueZero演算,因此,Set中若已包含NaN,進一步add(NaN)的話,Set中還是只有一個NaN;要注意的是,Array的indexOf、lastIndexOf雖然採嚴格相等比較,然而,在ECMAScript 7新增的includes方法,卻是採用SameValueZero演算,這也使得同一個物件上,會有兩種相等比較的規格。

必須得留意的是,JavaScript的規格中,並沒有規範equals、hashCode之類在其他語言中常見的相等比較協定,因此,對於既有的Array或者是ECMAScript 6後的Set、Map,無法去判斷兩個物件的實質狀態是否相等。

在〈ECMAScript 6: maps and sets(https://goo.gl/ijyKYB)〉中,也提到這是特意的,因為JavaScript的特性易於變動,而一個可變物件在實作equals、hashCode之類協定時,往往容易出錯。

SameValue演算

不知道你是否留意過,實際在ECMAScript 5.1中,已經規範了SameValue演算,它將NaN的比對視為true,在ECMAScript 5.1中,實現SameValue演算的API有Object.is,而Object.is(NaN, NaN)會是true,另一重點就是Object.is(0, -0)會是false,其餘與嚴格相等比較相同。

也就是說,這個SameValue演算,與後來的SameValueZero演算之間,差別在於,後者將0與-0視為true。

同樣也採用SameValue演算的,還有ECMAScript 5.1的Object.defineProperty——如果物件特性透過Object.defineProperty,將之設為不可變動(immutable),那麼,試圖再由Object.defineProperty設為其他值,就會引發TypeError。

不過,若實際上Object.defineProperty時所設定的值,與原本的值相同,並不會引發TypeError,也就表示,若某個特性被Object.defineProperty,設為不可變動的-0,再次執行Object.defineProperty設為-0時,並不會有錯誤。然而,問題就在於,如果試圖把Object.defineProperty設為0,卻會引發TypeError,也就是說,Object.defineProperty認為0與-0是不相同的。

為什麼JavaScript要搞得這麼複雜呢?

在IEEE 754浮點數的規範裡面就說了,NaN不等於NaN,JavaScript中的數字都是浮點數,又容易發生型態轉換,產生NaN的機會更多;其他語言也有NaN,例如:Java中,0.0/0.0會產生NaN,Double.NaN.equals(Double.NaN),或甚至Double.NaN == Double.NaN,都會是false(因為Double.NaN是個double,其位元順序比對被視為false),也有個Double.isNaN用來判斷NaN。

-0在電腦中,主要用來表達浮點數在二進位編碼上,第一個位元是1,而在Java當中,將0.0與-0.0分別裝箱為Double,若以兩者用equals來相互比較,結果會是false(然而,double的0.0 == -0.0,卻又是true):

Double a = 0.0, b = -0.0;
out.println(a.equals(b)); // false

只是這類的特性,較少為開發者知曉罷了,SameValue與SameValueZero,大概是為此而產生的規範,在開發者普遍忽略NaN與-0的情況下,與其讓它們定義不明,不如明確規範下來。

清楚使用的是哪種相等性

稍做整理一下,在JavaScript的嚴格相等比較之中,NaN與NaN並不相等,然而,0與-0視為相等,SameValue演算會將NaN視為相等,但是,0與-0視為不相等,而在ES6的SameValueZero中,NaN視為相等,0與-0也視為相等。

無論如何,JavaScript現在有四種相等比較規範了,當與其他開發者溝通談及相等性時,實際上提到的是哪一種規範呢?

完全忽視IEEE 754浮點數規範的話,大概就是SameValueZero演算了,只不過怎麼沒將NaN視為相等,0與-0也視為不相等的規範呢?

此外,如果想要遵守IEEE 754,搞不好,未來的ECMAScript,又會規範SameValueIEEE754之類的相等演算,而這麼一來,可得先跟相關的開發者做好溝通了!

專欄作者

熱門新聞

Advertisement