---
title: "06 — Equality — == vs === vs Object.is, and Four Algorithms"
uid: equality-comparisons
tags: ["strict-equality", "roadmap:javascript", "coercion", "equality", "object-is", "javascript"]
excerpt: "There isn't one equality in JavaScript — there are four algorithms under the hood, and ==, ===, and Object.is each pick a different one, differing only on two edge cases."
date: 2026-08-13T03:28:06+0000
source: https://www.aveshina.my.id/en/blog/equality-comparisons
---

"Always use triple equals, move on" was my equality strategy, and it worked until it couldn't explain an edge case. The idea that everything else hangs off: **there isn't one equality in JavaScript, there are four algorithms under the hood, and ==, ===, and Object.is each pick a different one — differing only on two edge cases.** [1]

The framing that finally landed is the ladder of strictness. Reading from loosest to strictest:

```figure
<svg viewBox="0 0 740 280" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="Four equality algorithms as a ladder of strictness, from loosest to strictest. Loose == coerces types. Strict === no coercion, but NaN !== NaN and +0 === -0. SameValueZero treats NaN equal to itself. SameValue (Object.is) also distinguishes +0 from -0.">
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">
    <!-- header row -->
    <text x="180" y="22" font-size="10" font-weight="700" fill="#475569">algorithm</text>
    <text x="430" y="22" font-size="10" font-weight="700" fill="#475569" text-anchor="middle">NaN vs NaN</text>
    <text x="600" y="22" font-size="10" font-weight="700" fill="#475569" text-anchor="middle">+0 vs -0</text>

    <!-- Loose == -->
    <rect x="30" y="40" width="300" height="50" rx="8" fill="#fee2e2" stroke="#dc2626" stroke-width="1.5"/>
    <text x="180" y="62" font-size="13" font-weight="700" font-family="ui-monospace,monospace" fill="#7f1d1d" text-anchor="middle">==  (loose)</text>
    <text x="180" y="80" font-size="9.5" fill="#7f1d1d" text-anchor="middle">coerces types before comparing</text>
    <text x="430" y="70" font-size="11" fill="#7f1d1d" text-anchor="middle">false</text>
    <text x="600" y="70" font-size="11" fill="#7f1d1d" text-anchor="middle">true</text>

    <!-- Strict === -->
    <rect x="30" y="100" width="300" height="50" rx="8" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="180" y="122" font-size="13" font-weight="700" font-family="ui-monospace,monospace" fill="#1e1b4b" text-anchor="middle">===  (strict)</text>
    <text x="180" y="140" font-size="9.5" fill="#1e1b4b" text-anchor="middle">no coercion; types must match</text>
    <text x="430" y="130" font-size="11" fill="#1e1b4b" text-anchor="middle">false</text>
    <text x="600" y="130" font-size="11" fill="#1e1b4b" text-anchor="middle">true</text>

    <!-- SameValueZero -->
    <rect x="30" y="160" width="300" height="50" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="180" y="182" font-size="13" font-weight="700" font-family="ui-monospace,monospace" fill="#052e16" text-anchor="middle">SameValueZero</text>
    <text x="180" y="200" font-size="9.5" fill="#052e16" text-anchor="middle">used by Map, Set, Array.includes</text>
    <text x="430" y="190" font-size="11" fill="#052e16" text-anchor="middle">true</text>
    <text x="600" y="190" font-size="11" fill="#052e16" text-anchor="middle">true</text>

    <!-- SameValue / Object.is -->
    <rect x="30" y="220" width="300" height="50" rx="8" fill="#fce7f3" stroke="#db2777" stroke-width="1.5"/>
    <text x="180" y="242" font-size="13" font-weight="700" font-family="ui-monospace,monospace" fill="#500724" text-anchor="middle">SameValue — Object.is()</text>
    <text x="180" y="260" font-size="9.5" fill="#500724" text-anchor="middle">strictest; distinguishes the two zero signs</text>
    <text x="430" y="250" font-size="11" fill="#500724" text-anchor="middle">true</text>
    <text x="600" y="250" font-size="11" fill="#500724" text-anchor="middle">false</text>
  </g>
</svg>
```

## The two operators

**== (loose equality)** compares two values, performing type coercion if the types differ [2]. "5" == 5 is true because the string is converted to a number first. null == undefined is true (a special-case pair). This is the operator that produces "surprising" results — "0" == false is true, "" == 0 is true — because the Abstract Equality Comparison algorithm coerces both sides through a fixed set of rules. My rule is simple: never write ==. The surprises aren't worth the keystroke.

**=== (strict equality)** compares without coercion [3]. If the types differ, it returns false immediately. Same type, compare values directly. The one famous wrinkle: NaN === NaN is false, and +0 === -0 is true. This is the default for almost all equality checks.

The reason === wins isn't that it's stricter for its own sake — it's that the result is *predictable*. With == I had to hold the coercion table in my head; with === the question "are these the same type and value?" answers itself.

## Object.is — the strictest

Object.is() is the fourth algorithm, SameValue [4]. It behaves like === except it fixes the two edge cases the spec calls out:

- Object.is(NaN, NaN) → true (where NaN === NaN is false)
- Object.is(+0, -0) → false (where +0 === -0 is true)

In normal code these differences almost never matter — but when they do, Object.is is the only correct tool. The classic case is NaN: x === NaN is always false, so the idiomatic check is Number.isNaN(x) or, equivalently, Object.is(x, NaN).

## SameValueZero — the one you can't call directly

The fourth algorithm, **SameValueZero**, is the same as Object.is except it treats +0 and -0 as equal [5]. It's not exposed as a function — it's used *internally* by Map key comparison, Set membership, and Array.prototype.includes(). That's why [NaN].includes(NaN) is true but [NaN].indexOf(NaN) is -1 (indexOf uses ===). When a Set says "I already have this value," it's using SameValueZero under the hood.

The practical upshot: collection lookups are *slightly* smarter than ===. If I'm checking whether a value is in an array and NaN is a possibility, arr.includes(x) is correct where arr.indexOf(x) !== -1 is not.

## How I use this

The rules I actually apply: default to === for every comparison; reach for Object.is only when NaN or signed-zero is genuinely in play (and Number.isNaN for the NaN check reads cleaner); never write ==; and remember that Array.includes, Set, and Map use SameValueZero, which is more forgiving than === on NaN. That covers 100% of the equality decisions in real code. The four-algorithm view matters because it explains *why* the four differ — and once I could see the two edge cases (NaN and signed zero) as the only variables, equality stopped being a source of doubt.

## References

[1] Mozilla, "Equality comparisons and sameness," MDN Web Docs, 2024. [Online]. Available: [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness)

[2] Mozilla, "Equality (==) operator," MDN Web Docs, 2024. [Online]. Available: [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Equality](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Equality)

[3] Mozilla, "Strict equality (===) operator," MDN Web Docs, 2024. [Online]. Available: [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Strict_equality](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Strict_equality)

[4] Mozilla, "Object.is() — same-value equality," MDN Web Docs, 2024. [Online]. Available: [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is)

[5] Mozilla, "Same-value-zero equality," MDN Web Docs, 2024. [Online]. Available: [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness#same-value-zero_equality](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness#same-value-zero_equality)

```quiz
Q: Why is `==` generally avoided in favor of `===`?
- == coerces types before comparing, producing surprising results like "0" == false being true
- == is slower at runtime
correct: 0
explain: The loose == operator follows the Abstract Equality Comparison algorithm and coerces operands, which yields non-obvious results. === never coerces; differing types means false, immediately and predictably.

Q: `NaN === NaN` returns false. What's the idiomatic way to test for NaN?
- x === NaN
- Number.isNaN(x) (or Object.is(x, NaN))
correct: 1
explain: NaN is the only value not equal to itself under ===. Number.isNaN() checks for the NaN value safely; Object.is(x, NaN) works too because SameValue treats NaN as equal to itself.

Q: Why does [NaN].includes(NaN) return true but [NaN].indexOf(NaN) return -1?
- includes uses SameValueZero (NaN equals NaN); indexOf uses === (NaN does not equal NaN)
- includes is buggy; indexOf is correct
correct: 0
explain: Array.prototype.includes, Set, and Map use the SameValueZero algorithm, which treats NaN as equal to itself. indexOf uses strict ===, which does not.

Q: What distinguishes Object.is from ===?
- Object.is also treats NaN as equal to NaN, and distinguishes +0 from -0
- Object.is coerces types like == but returns a boolean
correct: 0
explain: Object.is is the SameValue algorithm. It agrees with === everywhere except: Object.is(NaN, NaN) is true, and Object.is(+0, -0) is false. Two edge cases, no coercion.

Q: Which statement about the four equality algorithms is correct?
- SameValueZero is the one used internally by Map key lookup and Array.includes
- SameValueZero is the same as == and coerces types
correct: 0
explain: SameValueZero is not directly callable; Map, Set, and Array.includes use it. It treats NaN equal to NaN and +0 equal to -0, with no type coercion.
```
