Javascript equality and sameness

This is the second post in my Javascript serial.

There are 3 types of comparison operators in Javascript

  • === (most recommend): strict comparison or identity comparison
  • == (not recommend): loose comparison
  • Object.is api: SameValue comparison. Added from ES6.

From my practical experience, === is the first comparison you should consider when choosing one from the above three types, as it works in every version of the Javascript implementation.

== is indeed discouraged in default ESlint configuration.

Object.is is added from ES6. You should check compatibility and apply polyfill if your client's browser does not support it.


What is the difference?

Because == is not so useful and it will raise my eslint warning when being used. I have never used or seen any use case of the loose comparison == operator in real life. Thus, I will not discuss the loose comparison operator here.

The main difference between === and Object.is is how do they handle these 3 special primitive number values: +0, -0, and NaN.

+0 and -0 have the same mathematical semantic meaning and they are the same in most mathematical operations. However, in fact, they are different values and have different byte presentations.

With strict comparison ===, +0 equals to -0, vice versa. NaN is not equal to itself ( 0 is interpreted as +0). While these comparisons' results are both reversed with Object.is comparison. Check ECMS ( strict equality, SameValue ) specification for reference.

0 === -0 // true
Object.is(0, -0) // false

1 / 0 // Infinity
1 / -0 // -Infinity

NaN === NaN // false
Object.is(NaN, NaN) // true

Why +0, -0, and NaN?

To understand this specification, you should have a look at double-precision binary floating-point format binary64. It is not required for you to read the reference to understand the following content in this paragraph.

IEEE 754 double-precision binary floating-point format: binary64

In Javascript, the fact that number is stored in IEEE754 format ( ref) implies that all numbers in Javascript have theirs own 64 bit (i.e. 8 bytes) presentations.

However, this does not mean that all 64-bit binary arrays have their own float value due to the specification of the IEEE754 format. In other words, the map from IEEE754 formatted number (or Javascript number) to 64-bit binary array is injective but not surjective.

Hence, there are many 64-bit binary arrays that do not have an equivalent numerical value. In Javascript, 2 of them are chosen to be Infinity and -Infinity. All other non-associated values are considered not-a-number ( NaN) instances. Thus, there is not only one but many NaN instances.

I could not find whether the ECMA specification defines these special number values' binary presentation from the specs. It is probably that the binary presentation varies from implementation to implementation.

Use this script to check for the binary presentation of an arbitrary number.

var f2b = x => new Uint8Array(new Float64Array([x]).buffer);
var b2f = x => new Float64Array(x.buffer)[0];
source MDN docs

f2b converts number value to an 8-length byte array, b2f converts Unit8Array instance to a number. Using f2b, we can check for default NaN instance, Infinity and -Infinity binary representations.

f2b(NaN) //Uint8Array(8) [0, 0, 0, 0, 0, 0, 248, 127]
f2b(Infinity) //Uint8Array(8) [0, 0, 0, 0, 0, 0, 240, 127]
f2b(+Infinity) //Uint8Array(8) [0, 0, 0, 0, 0, 0, 240, 127]
f2b(-Infinity) //Uint8Array(8) [0, 0, 0, 0, 0, 0, 240, 255]
f2b(0) //[0, 0, 0, 0, 0, 0, 0, 0]
f2b(+0) //[0, 0, 0, 0, 0, 0, 0, 0]
f2b(-0) //[0, 0, 0, 0, 0, 0, 0, 128]

Throughout the above discussion, we understand that in Javascript, the strict equal === operators give understandable and common sense operations. === considers all values are different from NaN even the NaN itself. And the only case in which x !== x is true is when x is NaN, in a sense that we do not know which instance the NaN value is.

There are 2 ways to check if a value is NaN in Javascript. Via Object.is(NaN, x) or isNaN(x).


Quirk

Check these expressions' results from your browser. This quirk requires your browser to support ES6 Array.prototype.includes().

let x = 'hello'
[x].includes(x) // true
[x].indexOf(x) // 0
x = NaN
[x].includes(x) // true
[x].indexOf(x) // -1

The result can be explained by the fact that .includes() use Object.is comparison, in the contrast of strict equal === comparison used by .indexOf().


Bonus image from an MDN article


In this post, I have shared my knowledge about equality comparison operators in Javascript. Mostly I discussed the strict comparison and SameValue comparison.

Please feel free to discuss.