JavaScript Interview Prep

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 handle NaN and -0.


Quick Reference

AlgorithmNaN === NaN0 === -0Primary Use
Strict Equalityfalsetrue===, !==, switch, indexOf
SameValuetruefalseObject.is(), Object.defineProperty
SameValueZerotruetrueMap, Set, includes()
SameValueNonNumericN/AN/AInternal helper for non-numeric types

Table of Contents


Why JavaScript Has Multiple Equality Algorithms

The IEEE-754 floating-point specification creates two edge cases that complicate equality:

  1. NaN (Not a Number) β€” Defined as not equal to itself (NaN !== NaN)
  2. Negative Zero (-0) β€” A distinct value from +0, but === treats them as equal

Different use cases require different handling of these edge cases:

Use CaseNaN 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:

  1. If Type(x) β‰  Type(y), return false
  2. If Type(x) is Number:
    • If x is NaN, return false
    • If y is NaN, return false
    • If x is same Number value as y, return true
    • If x is +0 and y is -0, return true
    • If x is -0 and y is +0, return true
    • Return false
  3. 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:

  1. If Type(x) β‰  Type(y), return false
  2. If Type(x) is Number:
    • If x is NaN and y is NaN, return true
    • If x is +0 and y is -0, return false
    • If x is -0 and y is +0, return false
    • If x is same Number value as y, return true
    • Return false
  3. 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:

  1. If Type(x) β‰  Type(y), return false
  2. If Type(x) is Number:
    • If x is NaN and y is NaN, return true
    • If x is +0 and y is -0, return true
    • If x is -0 and y is +0, return true
    • If x is same Number value as y, return true
    • Return false
  3. 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:

  1. Assert: Type(x) is not Number
  2. Assert: Type(x) equals Type(y)
  3. If Type(x) is Undefined, return true
  4. If Type(x) is Null, return true
  5. If Type(x) is String, return true iff x and y are the same sequence of code units
  6. If Type(x) is Boolean, return true iff both are true or both are false
  7. If Type(x) is Symbol, return true iff x and y are the same Symbol value
  8. If Type(x) is Object, return true iff x and y are 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 / OperatorAlgorithmCan Find NaN?Distinguishes Β±0?
===, !==Strict Equality❌ No❌ No
switch caseStrict 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:

  1. NaN should be findable β€” A Map with NaN as a key should be retrievable
  2. Β±0 distinction rarely matters for keys β€” Treating them as same key prevents confusion
  3. 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

ScenarioUse
General equality checks===
Need to detect NaNNumber.isNaN() or Object.is()
Need to distinguish Β±0Object.is()
Searching arrays for NaNincludes() (not indexOf())
Key lookups with NaNMap / Set
React/Redux shallow comparisonObject.is()
Property change detectionObject.is()

Interview Q&A

Q: What are the three equality algorithms in JavaScript?
  1. Strict Equality (===) β€” Returns false for NaN === NaN, treats Β±0 as equal
  2. SameValue β€” Returns true for Object.is(NaN, NaN), distinguishes Β±0
  3. SameValueZero β€” Returns true for NaN comparisons, treats Β±0 as equal
Q: Why does [NaN].indexOf(NaN) return -1 but [NaN].includes(NaN) return true?

They use different algorithms:

  • indexOf() uses Strict Equality (===), where NaN !== NaN
  • includes() uses SameValueZero, where NaN equals NaN

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, NaNfalsetrue
0, -0truefalse

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)
  • "" and false are 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