JavaScript Has 9 Ways to Check Equality (And They All Disagree)

JS Edition

March 18, 2026

Most languages have one way to check if two things are equal. Maybe two if you’re fancy.

JavaScript has nine.

And they all give different answers. On the same values.

Let me show you the madness.

The Basics (That Aren’t Basic)

Everyone knows about == and ===. Right?

5 == "5"   // true (type coercion)\n5 === "5"  // false (strict)

Simple enough. Use ===, avoid ==, move on with your life.

But then there’s this:

NaN === NaN        // false\nObject.is(NaN, NaN)  // true\n\n-0 === 0           // true\nObject.is(-0, 0)   // false

So === lies about two things:

  1. NaN is not equal to itself (except it is)

  2. Negative zero equals positive zero (except it doesn’t)

And Object.is() tells the truth. Except most of the time you want the lie.

This is fine. Everything is fine.

Wait, There Are NINE?

Here’s the full list of equality algorithms in JavaScript:

  1. == (Abstract Equality) - “Whatever, make it work”

  2. === (Strict Equality) - “Types must match (but I lie about NaN and -0)”

  3. Object.is() - “Actual equality”

  4. SameValueZero - “Like Object.is but 0 equals -0”

  5. SameValue - “Like Object.is (it’s the same thing)”

  6. < and > - “Relational comparison with coercion”

  7. <= and >= - “Wait these work differently”

  8. in operator - “Does property exist?”

  9. instanceof - “Prototype chain checking”

You don’t use #4, #5, #8, and #9 directly. JavaScript uses them internally. But they exist. And they disagree with each other.

Let me prove it.

The Comparison Chart From Hell

const x = NaN;\nconst y = NaN;\n\nx == y           // false\nx === y          // false\nObject.is(x, y)  // true

So are NaN values equal or not? Depends which algorithm you ask.

const a = 0;\nconst b = -0;\n\na == b           // true\na === b          // true\nObject.is(a, b)  // false

Are zeros equal? Also depends.

const arr = [NaN];\n\narr.includes(NaN)     // true (uses SameValueZero)\narr.indexOf(NaN)      // -1 (uses ===)\narr.find(x => x)      // NaN (you make the rules)

So includes() finds NaN, but indexOf() doesn’t. Same array. Same value. Different results.

This makes sense to exactly zero people.

The Type Coercion Disaster

Let’s talk about ==. The comparison operator that tries to be helpful and fails spectacularly.

0 == false        // true\n0 == ""           // true\n0 == "0"          // true\nfalse == ""       // true\nfalse == "0"      // true\n\n// But wait...\n"" == "0"         // false (???)

So by the transitive property of... wait, there is no transitive property.

If A equals B, and B equals C, does A equal C?

Not in JavaScript!

// A == B\n"" == 0           // true\n\n// B == C\n0 == "0"          // true\n\n// Therefore A == C?\n"" == "0"         // false\n\n// Math has left the chat

The Array Comparison Minefield

[] == []          // false (different objects)\n[] == ![]         // true (I'm sorry, what?)

Let me break down that second one:

  1. ![] converts to false (arrays are truthy, so ! makes it falsy)

  2. [] == false compares empty array to false

  3. Empty array converts to empty string ""

  4. Empty string converts to 0

  5. false converts to 0

  6. 0 == 0 is true

So [] == ![] is true because of a five-step type coercion chain.

This is why we can’t have nice things.

When JavaScript Uses Which Algorithm

Here’s what’s actually happening under the hood:

=== (Strict Equality):

  • Most comparisons you write

  • switch statements

  • Some array methods

SameValueZero:

  • Array.includes()

  • Map keys

  • Set values

  • String.includes()

SameValue (aka Object.is()):

  • Object.defineProperty()

  • Internal slot comparisons

  • Spec algorithms

Abstract Equality (==):

  • Your tech debt from 2012

  • Legacy code you’re afraid to touch

  • That one comparison you regret

Wait, what’s the difference between SameValue and SameValueZero?

// SameValue: -0 and 0 are different\nObject.is(-0, 0)  // false\n\n// SameValueZero: -0 and 0 are same\n[-0].includes(0)  // true (uses SameValueZero)

One treats -0 and 0 as different. The other treats them as the same.

That’s the only difference. That’s why we have two separate algorithms.

Because apparently one algorithm that handles negative zero wasn’t complicated enough.

The Real-World Impact

I’ve been coding JavaScript since the jQuery days. Here’s when these differences actually matter:

Never:

  • SameValue vs SameValueZero differences

  • Comparison algorithm implementation details

  • The existence of == (just don’t use it)

Rarely:

  • NaN comparisons (use Number.isNaN())

  • -0 comparisons (pretend it doesn’t exist)

  • Object.is() for anything

Always:

  • Just use === and move on

The one exception:

// Checking if something is NaN\nObject.is(value, NaN)  // Works\nvalue !== value         // Also works (NaN is the only value not equal to itself)\nNumber.isNaN(value)     // Best option

Let’s Test Your Understanding

What do these return?

// Test 1\nnull == undefined\n\n// Test 2\nnull === undefined\n\n// Test 3\n[1, 2, 3] == [1, 2, 3]\n\n// Test 4\n[1, 2, 3] === [1, 2, 3]\n\n// Test 5\ntypeof NaN === "number"

Answers:

  1. true - == considers null and undefined equal

  2. false - === doesn’t

  3. false - Arrays are objects, compared by reference

  4. false - Still comparing references

  5. true - NaN is technically a number (even though it means “Not a Number”)

If you got all five right, you either:

  • Read the spec for fun

  • Have been traumatized by bugs

  • Are lying

The Type Coercion Table You’ll Never Memorize

Here’s what == does when comparing different types:

undefined == null        // true (special case)\nnumber == string         // Convert string to number\nboolean == anything      // Convert boolean to number (true=1, false=0)\nobject == primitive      // Call valueOf() or toString()

This creates gems like:

true == "1"              // true (boolean→1, "1"→1)\ntrue == "2"              // false (boolean→1, "2"→2)\n"0" == false             // true ("0"→0, false→0)\n"00" == false            // true ("00"→0, false→0)\n"0" == "00"              // false (both strings, compared directly)

The transitive property is weeping.

Objects Are Never Equal (Except When They Are)

const obj1 = {a: 1};\nconst obj2 = {a: 1};\n\nobj1 == obj2   // false\nobj1 === obj2  // false

Objects compare by reference, not value. Different objects are never equal.

const obj3 = obj1;\n\nobj1 === obj3  // true (same reference)

But wait:

const obj4 = Object.create(null);\nconst obj5 = Object.create(null);\n\nobj4 == obj5   // false (still different objects)

Even objects with no prototype aren’t equal.

The only way objects are equal is if they’re literally the same object.

Which makes sense, but then you see this:

[] == ""       // true\n[1] == "1"     // true\n[1,2] == "1,2" // true

Arrays convert to strings for comparison. So arrays can equal strings. But two identical arrays aren’t equal to each other.

[1] == [1]     // false (different objects)\n[1] == "1"     // true (converts to string)

Logic is not JavaScript’s strong suit.

The Philosophical Question

Is NaN equal to NaN?

IEEE 754 says: No. NaN represents “undefined” or “unrepresentable” values. Two undefined values aren’t necessarily the same undefined value.

JavaScript says: Depends which function you ask.

NaN === NaN              // false\nObject.is(NaN, NaN)      // true\n[NaN].includes(NaN)      // true\n[NaN].indexOf(NaN)       // -1

So NaN is not equal to itself, except when it is, and you can find it in an array, except when you can’t.

My philosophy: NaN is JavaScript’s way of telling you “something went wrong, but I’m not going to tell you what.”

It’s the blue screen of death, but for numbers.

Practical Advice (Finally)

After all this madness, here’s what you actually do:

1. Use === for everything

if (x === y) {\n  // Works 99.9% of the time\n}

2. Check for NaN specifically

if (Number.isNaN(x)) {\n  // The right way\n}\n\n// Not this:\nif (x !== x) {\n  // Clever but confusing\n}

3. Avoid == like it’s cursed

Because it is.

if (x == y) {\n  // Code review comment: "pls use ==="\n}

4. For objects, use a library

import { isEqual } from 'lodash';\n\nisEqual(obj1, obj2);  // Deep equality

5. Forget everything else exists

You don’t need SameValueZero. You don’t need SameValue. You don’t need to know they exist.

Unless you’re reading this article. In which case, sorry, you know now.

The Mental Model

Think of JavaScript’s equality like this:

  • ==: “These are kinda the same if you squint”

  • ===: “These are exactly the same (except NaN and -0)”

  • Object.is(): “These are actually exactly the same”

  • Everything else: “Internal spec stuff you shouldn’t worry about”

Or, simplified:

Just use === and be mildly confused sometimes.

The Final Boss

Okay, last test. What does this return?

const weird = {\n  valueOf() {\n    return 3;\n  },\n  toString() {\n    return "3";\n  }\n};\n\nweird == 3;

Think about it.

The answer is: true

When comparing an object to a primitive with ==, JavaScript calls valueOf() first. The object becomes 3, which equals 3.

But:

weird === 3;  // false

Because === doesn’t do type coercion. The object stays an object.

This is why we don’t use ==.

How Other Languages Handle This

Python:

# Just two ways\nx == y   # Equality\nx is y   # Identity (same object)

Simple. Sane. Boring.

Ruby:

x == y       # Value equality\nx.eql?(y)    # Hash equality\nx.equal?(y)  # Object identity

Three ways. Reasonable.

Java:

x == y         // Reference equality\nx.equals(y)    // Value equality

Two ways. Clear purpose for each.

JavaScript:

Nine ways. Complete chaos. Choose your own adventure. Nothing makes sense. Welcome to hell.

My Honest Take

JavaScript’s equality is a disaster because of two design decisions in 1995:

  1. == tries to be helpful - Type coercion seemed like a good idea at the time. It wasn’t.

  2. IEEE 754 compliance - NaN and -0 behave mathematically correctly but practically confusingly.

These decisions made sense in isolation. Together, they created a Gordian knot of comparison operators that trip up beginners and experts alike.

The solution: Pretend only === exists. Use Number.isNaN() for NaN. Ignore -0. Use lodash’s isEqual() for objects.

Don’t think about the other six algorithms. They can’t hurt you if you don’t use them.

Mostly.


Try it yourself:

Open your console and run:

// The trilogy of confusion\nconsole.log(NaN === NaN);        // false\nconsole.log([] == ![]);          // true  \nconsole.log("" == "0");          // false\nconsole.log(0 == "");            // true\nconsole.log(0 == "0");           // true\n\n// Bonus round\nconsole.log(typeof NaN);         // "number"\nconsole.log(NaN !== NaN);        // true\nconsole.log([NaN].includes(NaN)); // true\nconsole.log([NaN].indexOf(NaN)); // -1\n\n// You can never unknow this

Other JavaScript crimes:

  • typeof null === "object"

  • [] + [] === ""

  • [] + {} !== {} + []

  • Math.max() < Math.min()

The language is held together by duct tape and bad decisions from 1995.


Which JavaScript comparison broke your code? Hit reply with your war stories.

Thanks for reading Under The Hood! Subscribe for free to receive new posts and support my work.

Leave a comment