JavaScript Equality Algorithms: ===, Object.is, and SameValueZero
Choose the right JavaScript equality rule for NaN, signed zero, array searches, and Map or Set keys.
JavaScript does not use one equality rule everywhere. === uses Strict Equality, Object.is exposes SameValue, and Map, Set, and Array.prototype.includes use SameValueZero. For most values they agree. The meaningful differences are NaN and signed zero.
The practical rule is simple. Use === for ordinary control flow, Object.is when a change detector must distinguish -0 from 0 or treat NaN as unchanged, and collection APIs when membership is the operation you actually need.
The three rules differ in two cases
The ECMAScript 2026 comparison algorithms define the behavior precisely.
| Comparison | NaN with NaN |
0 with -0 |
|---|---|---|
Strict Equality, used by === |
false | true |
SameValue, used by Object.is |
true | false |
SameValueZero, used by keyed collections and includes |
true | true |
Everything else follows the familiar pattern. Primitives of different types are unequal. Equal strings contain the same UTF-16 code units. Objects compare by identity, not by structure.
console.log(NaN === NaN); // false
console.log(Object.is(NaN, NaN)); // true
console.log(0 === -0); // true
console.log(Object.is(0, -0)); // false
const first = { id: 1 };
const second = { id: 1 };
console.log(first === second); // false
console.log(Object.is(first, first)); // true
Object.is does not compare object contents. Its name is easy to overread. Apart from the two numeric cases, it behaves like ===.
=== is the default comparison
Strict Equality does not coerce either operand. That makes it a good default for branches and guards.
console.log(1 === '1'); // false
console.log(false === 0); // false
console.log(null === undefined); // false
Loose equality, ==, is a fourth algorithm with conversion rules. It can be useful when an API intentionally accepts more than one representation, but it is not part of the three-way choice discussed here. Replacing every == with Object.is would change far more than coercion because the numeric edge cases differ too.
The NaN behavior comes from IEEE 754 semantics. A failed numeric conversion often needs an explicit Number.isNaN(value) test. The global isNaN function coerces first, so it answers a different question.
Object.is fits change detection
Some code needs to know whether a newly computed value is observably the same as the previous value. SameValue is useful there because repeated NaN values do not look like endless changes, while a sign change on zero remains visible.
function didChange(previous, next) {
return !Object.is(previous, next);
}
console.log(didChange(NaN, NaN)); // false
console.log(didChange(0, -0)); // true
console.log(didChange('a', 'a')); // false
Signed zero rarely matters in application data, but it can preserve directional information. For example, 1 / 0 is Infinity, while 1 / -0 is -Infinity. A numerical cache that treats those inputs as interchangeable may return a result with the wrong sign.
Conversely, choosing Object.is for a general membership check often creates an unwanted distinction. Most collections should treat both zero signs as the same key.
Collections use SameValueZero
The Set specification names SameValueZero as its distinct-value rule. Map keys follow the same keyed-collection semantics. This lets a collection reliably store and find NaN.
const values = new Set([NaN, NaN, 0, -0]);
console.log(values.size); // 2
console.log(values.has(NaN)); // true
const map = new Map();
map.set(NaN, 'not a number');
map.set(-0, 'zero');
console.log(map.get(NaN)); // not a number
console.log(map.get(0)); // zero
Objects still use identity as keys. Two separately created objects remain distinct even if their own properties match. To group records by an identifier, use the identifier itself as the key or build a canonicalization layer.
includes and indexOf intentionally disagree
The array search algorithms in ECMAScript 2026 give Array.prototype.includes a SameValueZero membership test. Array.prototype.indexOf uses Strict Equality because it returns the position of a strict match. The difference is visible with NaN.
const readings = [3, NaN, 7];
console.log(readings.includes(NaN)); // true
console.log(readings.indexOf(NaN)); // -1
console.log(readings.findIndex(Number.isNaN)); // 1
Use includes for a yes-or-no membership question. If the position matters and NaN is possible, use findIndex with an explicit predicate. Both array methods treat 0 and -0 as equal, so signed zero does not separate them.
Structural equality is application code
None of these algorithms recursively compares arrays, plain objects, dates, maps, or sets. JSON.stringify(a) === JSON.stringify(b) is not a general structural-equality algorithm either. Property order, unsupported values, cycles, prototypes, and type-specific state all affect that shortcut.
Define structural equality around the data model. A coordinate may compare two numeric fields. A syntax tree may require recursive node comparison. A domain identifier may make every other field irrelevant.
Choosing equality becomes easier once the operation has a name. Control-flow comparison points to ===. Change detection may call for Object.is. Membership belongs to includes, Map, or Set, which already apply SameValueZero.