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