JavaScript 的相等性

August 12, 2022

在〈弱型別的代價 – WAT!〉中談過,JavaScript 中的相等性比較,主要有 =====,為了避免自動型態轉換來搗蛋,建議使用 === 進行嚴格的相等比較。

在〈[與眾不同的資料型態](/basics/type/〉中談過 NaN,這是個麻煩的傢伙,NaN === NaN 會是 false,只能使用 isNaN 函式來判定一個值是不是 NaN,偏偏 isNaN 又不是那麼可靠。在 ES6 以後有了 Number.isNaN,在〈增強的數值與字串〉看過,現在只有 NaN 能讓 Number.isNaN 結果為 true 了。

然而,現在必須得問的是,在談到兩個值的相等性時,到底要像 ===NaN 視為 false,還是要像 Number.isNaN(或者 isNaN),將 NaN 視為 true 呢?

SameValueZero

例如,陣列中如果有 NaN,你知道 indexOflastIndexOf 找得到還是會傳回 -1?在 case 比對時,如果 case NaN,那麼這會是個有效案例嗎?如果一個 Set 包含了 NaN,那麼再 add 一個 NaN,到底要接受還是不接受呢?

陣列的 indexOflastIndexOf 採用的是 ===,因此試圖找 NaN 的話,會傳回 falsecase 也是使用 === 比較,因此 case NaN 是比對不到的:

> switch(NaN) {
...     case NaN:
...         console.log('NaN'); break;
...     default:
...         console.log('default');
... }
default
undefined
>

然而,SetMap 在比較相等性時,若有兩個 NaN 要比對,會視為相等,除此之外,行為跟 === 相同,在 ES6 以後,稱這種相等演算為 SameValueZero,以有別於單純地使用 === 時的相等運算。

SameValue

好吧!反正就是 NaN 的問題嘛!多記一種就是了,是這樣的嗎?來看看 Object.defineProperty 的行為:

> let o = {};
undefined
> Object.defineProperty(o, 'READONLY_N',
...     { value: -0, writable: false, configurable: false, enumerable: false });

{}
> Object.defineProperty(o, 'READONLY_N',
...     { value: 100, writable: false, configurable: false, enumerable: false })
;
TypeError: Cannot redefine property: READONLY_N
    at Function.defineProperty (<anonymous>)
    at repl:1:8
    at ContextifyScript.Script.runInThisContext (vm.js:50:33)
    at REPLServer.defaultEval (repl.js:240:29)
    at bound (domain.js:301:14)
    at REPLServer.runBound [as eval] (domain.js:314:12)
    at REPLServer.onLine (repl.js:441:10)
    at emitOne (events.js:121:20)
    at REPLServer.emit (events.js:211:7)
    at REPLServer.Interface._onLine (readline.js:282:10)
> Object.defineProperty(o, 'READONLY_N',
...     { value: -0, writable: false, configurable: false, enumerable: false });

{}
>

一個特性如果被 Object.defineProperty 定義為唯讀,那麼就不能改變該特性的值,否則發生 TypeError,然而,如果 Object.defineProperty 時實際上並沒有變動值,像上例那樣,還是指定為 -0 的話,並不會有 TypeError,那麼,將之定義為 0 呢?

> Object.defineProperty(o, 'READONLY_N',
...     { value: 0, writable: false, configurable: false, enumerable: false });
TypeError: Cannot redefine property: READONLY_N
    at Function.defineProperty (<anonymous>)
    at repl:1:8
    at ContextifyScript.Script.runInThisContext (vm.js:50:33)
    at REPLServer.defaultEval (repl.js:240:29)
    at bound (domain.js:301:14)
    at REPLServer.runBound [as eval] (domain.js:314:12)
    at REPLServer.onLine (repl.js:441:10)
    at emitOne (events.js:121:20)
    at REPLServer.emit (events.js:211:7)
    at REPLServer.Interface._onLine (readline.js:282:10)
>

驚喜!在這種情況下,0 與 -0 被視為不相等的喔!SameValueZero 演算將 0 與 -0 視為相等,而將 0 與 -0 視為不相等的演算,稱為 SameValue,這其實在 ES5 時就規範了,Object.is 採用的就是 SameValue 演算,因此 Object.is(0, -0) 的結果會是 false

> Object.is(0, -0);
false
>

SameValue 演算會將 NaN 視為相等,然而 0 與 -0 視為不相等,而 ES6 以後的 SameValueZero 會將 NaN 視為相等,0 與 -0 也視為相等。

總之,JavaScript 有四種相等性比較耶!好歡樂 !@#$%^ … XD

基本上,大多數情況,還是使用 === 就好了,然而,如果要面對 NaN0-0 的時候,最好是搞清楚自己打算使用哪種相等性,而使用 API 時,也查清楚它會使用哪種相等性。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter