Why JavaScript Equality Is More Complicated Than == vs ===
JavaScript has four equality algorithms. How == coerces, why NaN and -0 break ===, what Object.is and SameValueZero do, and which one Map, Set and includes use.
- 7 menit baca
Halaman ini belum tersedia dalam Bahasa Indonesia, jadi Anda membaca versi asli bahasa Inggris.
“Use === instead of ==” is good advice, but it is only half the story. JavaScript has four equality algorithms, and the built-ins don’t all use the same one: indexOf and includes disagree about NaN, Set treats 0 and -0 as one value, and objects are never equal unless they are the same object. This post lays out all four, how == actually converts types, and which comparison to use when.
Why It Matters
Equality bugs hide in:
- deduplication: a
new Setof user IDs that are sometimes numbers, sometimes strings - cache keys: objects that “look the same”
- React dependency arrays: a new object every render looks like a change
- form validation:
"0"vs0 - numeric code that produces
NaNor-0
Knowing which algorithm runs explains all of them.
The Four Algorithms
| Algorithm | Used by | Converts types? | NaN equals NaN? | 0 equals -0? |
|---|---|---|---|---|
| Loose equality | == | Yes | No | Yes |
| Strict equality | ===, indexOf, switch | No | No | Yes |
| SameValueZero | includes, Map, Set | No | Yes | Yes |
| SameValue | Object.is | No | Yes | No |
All four compare objects (including arrays and functions) by identity: two objects are equal only if they are the same object in memory.
NaN === NaN; // false
Object.is(NaN, NaN); // true
[NaN].indexOf(NaN); // -1 (strict equality)
[NaN].includes(NaN); // true (SameValueZero)
new Set([NaN, NaN]).size; // 1
0 === -0; // true
Object.is(0, -0); // false
new Set([0, -0]).size; // 1How == Converts
Loose equality converts values until both sides have the same type, then compares them. Simplified, it follows this order:
Each arrow to the right is a “yes”; a “no” moves down to the next question. Applied to the famous examples:
null == undefined; // true: the special pair
null == 0; // false: null only equals undefined
null >= 0; // true: relational operators use a different rule (ToNumber, so 0 >= 0)
'' == 0; // true: '' becomes 0
'0' == false; // true: false becomes 0, then '0' becomes 0
[] == ''; // true: [] becomes ''
[] == ![]; // true: ![] is false → 0, and [] → '' → 0
({}) == '[object Object]'; // true: the object becomes its string formnull >= 0 being true while null == 0 is false shows that == is not just “the comparison operators, but for equality”: it has its own rules.
Famous == Puzzles, Explained
The same few rules explain every “JavaScript is weird” example. Each result below was checked by running it:
| Expression | Result | Why |
|---|---|---|
[] == false | true | false → 0; [] → '' → 0; 0 == 0 |
[] == ![] | true | ![] is false (objects are truthy), then as above |
[0] == false | true | [0] → '0' → 0; false → 0 |
'' == 0 | true | The empty string becomes 0 |
' \t\n' == 0 | true | A string of only whitespace also becomes 0 |
'' == '0' | false | Both are strings, so nothing converts: they are just different strings |
'0' == false | true | false → 0, '0' → 0, even though '0' is truthy in an if |
true == 'true' | false | true → 1, and 'true' → NaN |
true == '1' | true | true → 1, '1' → 1 |
null == false | false | null only equals null and undefined; it is never converted |
undefined == 0 | false | Same rule as null |
NaN == NaN | false | NaN equals nothing, not even itself |
[1, 2] == '1,2' | true | The array becomes the string '1,2' |
[] == [] | false | Two different objects; no conversion between objects |
Two patterns cover most of them: booleans turn into numbers before anything else, and objects turn into strings (an array joins its items). That is why '' == '0' is false while '' == 0 and '0' == 0 are both true: == isn’t transitive.
The Two Values That Break ===
NaN
NaN (“not a number”) comes from failed numeric conversions and invalid math, and it is the only value not equal to itself under ===, so x === NaN is always false.
- Check with
Number.isNaN(x)orObject.is(x, NaN). - Avoid the global
isNaN, which converts first, soisNaN('hello')is true.
-0
JavaScript numbers follow IEEE 754, which has a positive and a negative zero. === treats them as equal, which is usually right, but they behave differently in division and a few functions:
1 / -0; // -Infinity
Math.sign(-0); // -0
String(-0); // '0': hidden when printed
Object.is(-0, 0); // false-0 turns up from rounding negative values (Math.round(-0.4)) and multiplying zero by a negative number. It matters for coordinates, animation direction and anything that divides. Object.is is the only operator that tells them apart.
Objects: Identity, Not Contents
[1, 2] === [1, 2]; // false: two different arrays
const a = { id: 1 };
const b = a;
a === b; // true: same objectThis identity rule is what React, Map and Set rely on:
SetandMapkeys compare by identity (SameValueZero), sonew Set([{ id: 1 }, { id: 1 }])has two entries. Use the id as the key instead.- React dependency arrays compare with
Object.is. An object or function created during render is “new” every time and re-runs the effect. Depend on primitives, or memoize the object. - Deep equality has no built-in operator. Compare the fields that matter, or use a tested helper (for example
assert.deepStrictEqualin Node.js,expect().toEqual()in tests).
Going Deeper: Equality Beyond Numbers and Objects
Which Built-ins Use Which Algorithm
You rarely call an equality algorithm by name; built-ins choose for you. This map explains most “why didn’t it find it?” bugs:
Two consequences worth remembering: switch (NaN) { case NaN: … } never matches, because switch uses strict equality; and React compares state and dependencies with Object.is, so a new object with the same contents always counts as a change.
Strings That Look Equal but Aren’t
Unicode lets some characters be written in more than one way. “é” can be one code point (U+00E9) or an “e” followed by a combining accent (U+0065 U+0301). They render identically, but they are different strings:
const composed = '\u00e9';
const decomposed = 'e\u0301';
composed === decomposed; // false
composed.length; // 1, while decomposed.length is 2
composed.normalize() === decomposed.normalize(); // true
composed.localeCompare(decomposed); // 0: equal for sorting and displayThis shows up with text from different keyboards, operating systems (macOS file names are often decomposed) and copy and paste. Normalize user-facing text (usually to NFC) before storing it, using it as a key or comparing it, and use localeCompare or Intl.Collator for sorting and case-insensitive comparison.
BigInt, Symbol and Dates
- BigInt:
1n == 1istrue, but1n === 1isfalse, because they are different types. Don’t mix BigInt and Number in maps or sets. - Symbol: every
Symbol('x')is unique, so two with the same description are never equal.Symbol.for('x')returns the same shared symbol each time. - Date: dates are objects, so
new Date(0) == new Date(0)isfalse. Compare their timestamps instead:a.getTime() === b.getTime().
Which Comparison to Use
| You want to check | Use |
|---|---|
| Almost anything | === |
Missing value (null or undefined) | value == null |
Is it NaN? | Number.isNaN(value) |
Exact identity, including -0 | Object.is(a, b) |
Is this value in an array (incl. NaN)? | array.includes(value) |
| Same contents in two objects | Compare fields, or a deep-equality helper |
| Strings that look the same in different languages | a.localeCompare(b, undefined, { sensitivity: 'base' }) === 0 or normalize() first |
Test Yourself
Predict each output, then open the answer. Every answer was checked by running the code.
Tantangan 1
An Empty Array
console.log([] == false, Boolean([]));Lihat jawabanSembunyikan jawaban
true true
[] == falseconvertsfalseto0, then[]to''and then to0, so0 == 0.Boolean([])istrue: every object is truthy.- So an empty array equals
falseand is still truthy in anif.
Tantangan 2
`null` and Zero
console.log(null == 0, null >= 0, undefined == 0, undefined >= 0);Lihat jawabanSembunyikan jawaban
false true false false
==has a special rule:nullonly equalsnullandundefined.- Relational operators convert to numbers instead:
nullbecomes0, so0 >= 0is true. undefinedbecomesNaN, and every comparison withNaNis false.
Tantangan 3
How Big Is the Set?
const values = new Set([1, '1', 1.0, NaN, NaN, 0, -0]);
console.log(values.size);Lihat jawabanSembunyikan jawaban
4: 1, '1', NaN and 0
1.0is the same number as1.- SameValueZero treats
NaNas equal to itself and0as equal to-0. - No algorithm except
==treats the string'1'as equal to the number1.
Tantangan 4
Three Ways to Look for NaN
const list = [NaN];
console.log(list.indexOf(NaN), list.includes(NaN), list.findIndex(Number.isNaN));Lihat jawabanSembunyikan jawaban
-1 true 0
indexOfuses strict equality, which never matchesNaN.includesuses SameValueZero, which does.findIndexuses whatever test you give it.
Tantangan 5
Wrapper Objects
const a = 'hi';
const b = new String('hi');
console.log(a === 'hi', b === 'hi', b == 'hi', typeof b);Lihat jawabanSembunyikan jawaban
true false true object
new Stringcreates an object wrapper, so===compares a string with an object and fails.==converts the object back to its primitive value and succeeds.- Never create wrappers with
new String,new Numberornew Boolean.
Tantangan 6
Comparing Objects by Content
const left = { x: 1, y: 2 };
const right = { y: 2, x: 1 };
console.log(left == right, JSON.stringify(left) === JSON.stringify(right));Lihat jawabanSembunyikan jawaban
false false
- Objects are compared by identity, so two separate objects are never equal.
JSON.stringifykeeps the order in which keys were added, so the same data in a different order produces different strings.- Compare the fields you care about, or use a deep-equality helper.
Key Takeaways
- JavaScript has four equality algorithms:
==,===, SameValueZero and SameValue (Object.is). ==converts types in a fixed order, which produces surprises like[] == ![]; avoid it except forvalue == null.===saysNaN !== NaNand0 === -0;Object.iscorrects both, andincludes,MapandSetfindNaN.- Objects are compared by identity under every algorithm, so compare contents explicitly or use stable keys.
- Use
Number.isNaN, not the globalisNaN, which converts its argument first.
Equality interacts with the object model: what happens when you read a property covers how objects are built, and the hidden cost of mutable state explains why identity comparisons are how UI frameworks detect change.
Sources
- MDN: String.prototype.normalize()
- MDN: Intl.Collator
- React docs: useState: Updating objects and arrays
- ECMAScript specification: IsLooselyEqual, SameValue and SameValueZero
- MDN: Equality comparisons and sameness
- MDN: Number.isNaN()
- React docs: useEffect dependencies
- ESLint: eqeqeq
Tag
Pertanyaan yang sering diajukan
Is == ever the right choice?
YesIn one case. The one common exception is value == null, which matches both null and undefined in a single check. Everywhere else, === is clearer, because == follows conversion rules most readers don’t remember.
How do I compare two objects or arrays by content?
Write the comparison for your data, compare a stable key such as an id, or use a deep-equality helper from your test library or a utility package. JSON.stringify works only for simple data and depends on key order.
Why does [NaN].indexOf(NaN) return -1 but [NaN].includes(NaN) return true?
indexOf uses strict equality, where NaN is never equal to itself. includes uses SameValueZero, which treats NaN as equal to NaN.
How does React decide that state changed?
It compares the old and new value with Object.is. Primitives with the same value are equal; objects and arrays are equal only if they are the same object, so updating state requires a new object or array.
Why are two identical-looking strings not equal?
They can use different Unicode sequences for the same character, such as a precomposed é and an e followed by a combining accent. Normalize both with normalize() (NFC) before comparing or storing them.
How do I compare strings without caring about case or accents?
Use localeCompare or Intl.Collator with sensitivity set to “base”, for example a.localeCompare(b, undefined, { sensitivity: 'base' }) === 0. Lowercasing alone fails for some languages and ignores accents.
How do I compare two dates?
Compare their timestamps, for example a.getTime() === b.getTime(), because two Date objects are always different objects. For “same day” checks, compare the formatted date in the relevant time zone.
Does the linter catch == mistakes?
YesESLint’s eqeqeq rule flags == and !=, and can allow the x == null idiom with the option “null”: “ignore”. TypeScript also reports comparisons between types that can never be equal.