JavaScript Equality Algorithms: ===, SameValue, and SameValueZero
🎯 Interview-Ready Deep Dive — JavaScript has multiple equality algorithms, not just ===. Understanding which algorithm JavaScript uses in different contexts is essential for predictable code and confident interview answers.
JavaScript Equality Algorithms: ===, SameValue, and SameValueZero
JavaScript has multiple equality algorithms — not just ===. Understanding which algorithm JavaScript uses in different contexts is essential for predictable code and confident interview answers.
TL;DR: JavaScript defines three primary comparison algorithms: Strict Equality (
===), SameValue (Object.is()), and SameValueZero (Map,Set,includes()). They differ primarily in how they handleNaNand-0.
Quick Reference
| Algorithm | NaN === NaN | 0 === -0 | Primary Use |
|---|---|---|---|
| Strict Equality | false | true | ===, !==, switch, indexOf |
| SameValue | true | false | Object.is(), Object.defineProperty |
| SameValueZero | true | true | Map, Set, includes() |
| SameValueNonNumeric | N/A | N/A | Internal helper for non-numeric types |
Table of Contents
- Why JavaScript Has Multiple Equality Algorithms
- The Four Comparison Algorithms
- Deep Dives
- Which API Uses Which Algorithm?
- Why Map, Set, and includes() Behave Differently
- Interview Q&A
- Interview Self-Check
Why JavaScript Has Multiple Equality Algorithms
The IEEE-754 floating-point specification creates two edge cases that complicate equality:
- NaN (Not a Number) — Defined as not equal to itself (
NaN !== NaN) - Negative Zero (-0) — A distinct value from
+0, but===treats them as equal
Different use cases require different handling of these edge cases:
| Use Case | NaN Should Equal NaN? | ±0 Should Be Distinct? |
|---|---|---|
General comparison (===) | No (IEEE-754 rule) | No (rarely matters) |
Value identity (Object.is) | Yes (same bit pattern) | Yes (different bits) |
Collection keys (Map/Set) | Yes (must be findable) | No (confusing otherwise) |
The Comparison Algorithms (and One Internal Helper)
1. Strict Equality (===)
The most common comparison operator. Follows IEEE-754 for numeric edge cases.
Algorithm Steps:
- If
Type(x)≠Type(y), returnfalse - If
Type(x)is Number:- If
xisNaN, returnfalse - If
yisNaN, returnfalse - If
xis same Number value asy, returntrue - If
xis+0andyis-0, returntrue - If
xis-0andyis+0, returntrue - Return
false
- If
- Return
SameValueNonNumeric(x, y)
Behavior:
NaN === NaN; // false — IEEE-754 rule
0 === -0; // true — treated as same value
"a" === "a"; // true — same primitive value
Used by: ===, !==, switch case matching, Array.prototype.indexOf(), Array.prototype.lastIndexOf()
// indexOf uses Strict Equality — can't find NaN
[NaN, 1, 2].indexOf(NaN); // -1 (not found!)
// switch uses Strict Equality
switch (NaN) {
case NaN: console.log("match"); break; // Never executes
default: console.log("no match"); // Always executes
}
2. SameValue
The most precise algorithm. Treats values as equal only if they are bit-identical.
Algorithm Steps:
- If
Type(x)≠Type(y), returnfalse - If
Type(x)is Number:- If
xisNaNandyisNaN, returntrue - If
xis+0andyis-0, returnfalse - If
xis-0andyis+0, returnfalse - If
xis same Number value asy, returntrue - Return
false
- If
- Return
SameValueNonNumeric(x, y)
Behavior:
Object.is(NaN, NaN); // true — same bit pattern
Object.is(0, -0); // false — different bit patterns
Object.is("a", "a"); // true
Used by: Object.is(), Object.defineProperty() (to detect whether a property’s value has changed)
// Object.is() is the ONLY way to distinguish -0 from 0
const velocity = -0;
velocity === 0; // true — hides the difference
Object.is(velocity, -0); // true — reveals it
Object.is(velocity, 0); // false
3. SameValueZero
A hybrid algorithm: treats NaN as equal to NaN (like SameValue) but treats ±0 as equal (like Strict Equality).
Algorithm Steps:
- If
Type(x)≠Type(y), returnfalse - If
Type(x)is Number:- If
xisNaNandyisNaN, returntrue - If
xis+0andyis-0, returntrue - If
xis-0andyis+0, returntrue - If
xis same Number value asy, returntrue - Return
false
- If
- Return
SameValueNonNumeric(x, y)
Behavior:
// Map uses SameValueZero
const map = new Map();
map.set(NaN, "found");
map.get(NaN); // "found" — NaN equals NaN for Map keys
map.set(0, "zero");
map.get(-0); // "zero" — 0 and -0 are same key
// Set uses SameValueZero
const set = new Set([NaN, NaN, 0, -0]);
set.size; // 2 — {NaN, 0}
// includes() uses SameValueZero
[NaN, 1, 2].includes(NaN); // true — can find NaN!
Used by: Map, Set, Array.prototype.includes(), TypedArray.prototype.includes()
4. SameValueNonNumeric
An internal helper algorithm used by all three above when comparing non-numeric types.
Algorithm Steps:
- Assert:
Type(x)is not Number - Assert:
Type(x)equalsType(y) - If
Type(x)is Undefined, returntrue - If
Type(x)is Null, returntrue - If
Type(x)is String, returntrueiffxandyare the same sequence of code units - If
Type(x)is Boolean, returntrueiff both aretrueor both arefalse - If
Type(x)is Symbol, returntrueiffxandyare the same Symbol value - If
Type(x)is Object, returntrueiffxandyare the same Object value (reference equality)
Key Insight: For non-numeric primitives, all three algorithms behave identically. The differences only matter for Number. BigInt does not have NaN or ±0, so all equality algorithms behave identically for BigInt values.
Deep Dives
Why NaN !== NaN
NaN === NaN; // false — this surprises most developers
Wait, isn’t NaN a primitive? Yes!
typeof NaN === "number"— NaN is a primitive number value, not an object. Yet it’s the only value in JavaScript that doesn’t equal itself. This exception is defined by the IEEE-754 floating-point specification, not JavaScript’s normal equality rules.
Why IEEE-754 Designed It This Way:
1. NaN Represents “Undefined Result”, Not a Single Value
NaN can arise from many different operations:
0 / 0; // NaN — indeterminate form
Math.sqrt(-1); // NaN — imaginary number
parseInt("abc"); // NaN — failed parsing
Infinity - Infinity; // NaN — indeterminate
Each NaN represents a different failure. Making them equal would falsely suggest they came from the same computation.
2. Self-Comparison Is a Detection Mechanism
The IEEE committee designed NaN so you can detect it with a simple check:
function isNaN_old(x) {
return x !== x; // Only NaN is not equal to itself
}
How to Properly Check for NaN:
// ❌ Don't use direct comparison
NaN === NaN; // false
// ✅ Option 1: Number.isNaN() — recommended
Number.isNaN(NaN); // true
Number.isNaN("hello"); // false (doesn't coerce)
// ✅ Option 2: Object.is()
Object.is(NaN, NaN); // true
// ⚠️ Avoid global isNaN() — it coerces first
isNaN("hello"); // true (coerces "hello" → NaN, then checks)
Why -0 Exists and When It Matters
JavaScript distinguishes between +0 and -0, but === treats them as equal:
0 === -0; // true — strict equality says they're the same
Object.is(0, -0); // false — they ARE different values
Why Does Negative Zero Exist?
-0 preserves the sign of the operand in mathematical operations:
// Division approaching zero from negative side
-1 / Infinity; // -0 (preserves negative sign)
1 / Infinity; // 0
// Sign is preserved through multiplication
-0 * 5; // -0
0 * -5; // -0
// Useful for directional calculations
Math.sign(-0); // -0 (preserves direction information)
When Does the Difference Matter?
// Division by zero considers the sign
1 / 0; // Infinity
1 / -0; // -Infinity — different result!
// String conversion hides the difference
String(-0); // "0" — hides it
(-0).toString(); // "0" — hides it
Object.is(-0, 0); // false — reveals it
Real-World Use Case: Direction Tracking
function getDirection(velocity) {
if (Object.is(velocity, -0)) {
return "stopped, was moving backward";
} else if (velocity === 0) {
return "stopped, was moving forward";
}
return velocity > 0 ? "forward" : "backward";
}
getDirection(-0); // "stopped, was moving backward"
getDirection(0); // "stopped, was moving forward"
Which API Uses Which Algorithm?
| API / Operator | Algorithm | Can Find NaN? | Distinguishes ±0? |
|---|---|---|---|
===, !== | Strict Equality | ❌ No | ❌ No |
switch case | Strict Equality | ❌ No | ❌ No |
Array.prototype.indexOf() | Strict Equality | ❌ No | ❌ No |
Array.prototype.lastIndexOf() | Strict Equality | ❌ No | ❌ No |
Object.is() | SameValue | ✅ Yes | ✅ Yes |
Object.defineProperty() | SameValue | ✅ Yes | ✅ Yes |
Array.prototype.includes() | SameValueZero | ✅ Yes | ❌ No |
TypedArray.prototype.includes() | SameValueZero | ✅ Yes | ❌ No |
Map.prototype.has() | SameValueZero | ✅ Yes | ❌ No |
Map.prototype.get() | SameValueZero | ✅ Yes | ❌ No |
Set.prototype.has() | SameValueZero | ✅ Yes | ❌ No |
Comparison Cheat Sheet:
// The three algorithms in action:
// Strict Equality (===)
NaN === NaN; // false
0 === -0; // true
[NaN].indexOf(NaN); // -1 (uses ===)
// SameValue (Object.is)
Object.is(NaN, NaN); // true
Object.is(0, -0); // false
// SameValueZero (Map, Set, includes)
[NaN].includes(NaN); // true
new Set([NaN, NaN]).size; // 1
new Map([[NaN, 1]]).get(NaN); // 1
new Set([0, -0]).size; // 1 (±0 collapsed)
Why Map, Set, and includes() Behave Differently
You might wonder: why can includes() find NaN but indexOf() can’t?
[NaN, 1, 2].indexOf(NaN); // -1 — NOT FOUND!
[NaN, 1, 2].includes(NaN); // true — FOUND!
The TC39 committee chose SameValueZero for collections because:
- NaN should be findable — A Map with NaN as a key should be retrievable
- ±0 distinction rarely matters for keys — Treating them as same key prevents confusion
- Consistency — All collections use the same algorithm
// This would be broken if Map used Strict Equality:
const cache = new Map();
cache.set(NaN, "computed value");
cache.get(NaN); // undefined ← useless!
// SameValueZero makes this work:
cache.get(NaN); // "computed value" ✓
Why Not SameValue for Collections?
If Map/Set used SameValue, you could have two zero keys:
// Hypothetical: if Map used SameValue
const map = new Map();
map.set(0, "positive zero");
map.set(-0, "negative zero");
map.size; // Would be 2 — confusing!
// Actual behavior with SameValueZero:
map.size; // 1 — -0 overwrites 0
When to Use Each
| Scenario | Use |
|---|---|
| General equality checks | === |
| Need to detect NaN | Number.isNaN() or Object.is() |
| Need to distinguish ±0 | Object.is() |
| Searching arrays for NaN | includes() (not indexOf()) |
| Key lookups with NaN | Map / Set |
| React/Redux shallow comparison | Object.is() |
| Property change detection | Object.is() |
Interview Q&A
Q: What are the three equality algorithms in JavaScript?
- Strict Equality (
===) — ReturnsfalseforNaN === NaN, treats±0as equal - SameValue — Returns
trueforObject.is(NaN, NaN), distinguishes±0 - SameValueZero — Returns
truefor NaN comparisons, treats±0as equal
Q: Why does [NaN].indexOf(NaN) return -1 but [NaN].includes(NaN) return true?
They use different algorithms:
indexOf()uses Strict Equality (===), whereNaN !== NaNincludes()uses SameValueZero, whereNaNequalsNaN
This design choice was intentional when includes() was added in ES2016 — TC39 wanted a method that could find NaN in arrays.
Q: What does Object.is() do that === doesn't?
Object.is() implements the SameValue algorithm, which differs from === in two ways:
| Comparison | === | Object.is() |
|---|---|---|
NaN, NaN | false | true |
0, -0 | true | false |
Use Object.is() when you need to detect NaN or distinguish ±0.
Q: Why does Map.get(NaN) work when NaN !== NaN?
Map uses SameValueZero for key comparisons, not Strict Equality. SameValueZero treats NaN as equal to NaN, so you can store and retrieve values using NaN as a key:
const map = new Map();
map.set(NaN, "found");
map.get(NaN); // "found"
Q: What algorithm does React use for comparing props?
React’s shallowEqual uses Object.is() (SameValue) to compare values. This means:
- NaN props are considered equal to NaN
- 0 and -0 props are considered different
This is why Object.is() was added to JavaScript — to provide consistent, predictable value comparison for frameworks like React and Redux.
Interview Self-Check
1. Predict the output:
const arr = [NaN, 0, -0];
console.log(arr.indexOf(NaN));
console.log(arr.includes(NaN));
console.log(arr.indexOf(-0));
Reveal Answer
arr.indexOf(NaN); // -1 (Strict Equality: NaN !== NaN)
arr.includes(NaN); // true (SameValueZero: NaN equals NaN)
arr.indexOf(-0); // 1 (Strict Equality: -0 === 0, finds at index 1)
2. What’s the size of this Set?
new Set([NaN, NaN, 0, -0, "", false]).size
Reveal Answer
4 — The Set contains {NaN, 0, "", false}
- Two NaNs collapse to one (SameValueZero: NaN equals NaN)
- 0 and -0 collapse to one (SameValueZero: ±0 are equal)
""andfalseare different types, both kept
3. Will this code ever log “found”?
switch (NaN) {
case NaN: console.log("found"); break;
default: console.log("not found");
}
Reveal Answer
No — switch uses Strict Equality, and NaN !== NaN. The default case always executes.
Summary Checklist
- Can name the three primary equality algorithms
- Knows which ECMAScript algorithm defines each
- Understands why NaN !== NaN (IEEE-754 design)
- Knows why -0 exists (sign preservation)
- Can explain why indexOf can’t find NaN but includes can
- Knows that Map/Set use SameValueZero
- Understands when to use Object.is()
- Can predict behavior of switch with NaN
Further Reading
- JavaScript Primitive Types — Understanding the 7 primitive types
- == vs === and Type Coercion — How Abstract Equality uses coercion before comparison
- ECMAScript: Strict Equality Comparison
- ECMAScript: SameValue