ข้ามไปที่เนื้อหา
Clinford Jordano Valencia
บทความทั้งหมด

JavaScript

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 นาที

หน้านี้ยังไม่มีฉบับภาษาไทย จึงแสดงต้นฉบับภาษาอังกฤษแทน

“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 Set of 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" vs 0
  • numeric code that produces NaN or -0

Knowing which algorithm runs explains all of them.

The Four Algorithms

AlgorithmUsed byConverts types?NaN equals NaN?0 equals -0?
Loose equality==YesNoYes
Strict equality===, indexOf, switchNoNoYes
SameValueZeroincludes, Map, SetNoYesYes
SameValueObject.isNoYesNo

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; // 1

How == Converts

Loose equality converts values until both sides have the same type, then compares them. Simplified, it follows this order:

Same type?Compare like ===null and undefined?trueEither is null/undefined?falseNumber vs string?Make the string a numberA boolean?Make it a number, compare againAn object?Make it a primitive, compare againyesno
The decisions behind x == y, simplified. The first matching rule decides; numbers win most conversions.

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 form

null >= 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:

ExpressionResultWhy
[] == falsetruefalse → 0; [] → '' → 0; 0 == 0
[] == ![]true![] is false (objects are truthy), then as above
[0] == falsetrue[0] → '0' → 0; false → 0
'' == 0trueThe empty string becomes 0
' \t\n' == 0trueA string of only whitespace also becomes 0
'' == '0'falseBoth are strings, so nothing converts: they are just different strings
'0' == falsetruefalse → 0, '0' → 0, even though '0' is truthy in an if
true == 'true'falsetrue → 1, and 'true' → NaN
true == '1'truetrue → 1, '1' → 1
null == falsefalsenull only equals null and undefined; it is never converted
undefined == 0falseSame rule as null
NaN == NaNfalseNaN equals nothing, not even itself
[1, 2] == '1,2'trueThe array becomes the string '1,2'
[] == []falseTwo 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) or Object.is(x, NaN).
  • Avoid the global isNaN, which converts first, so isNaN('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 object

This identity rule is what React, Map and Set rely on:

  • Set and Map keys compare by identity (SameValueZero), so new 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.deepStrictEqual in 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:

Looseconverts typesStrictNaN ≠ NaN, 0 = −0SameValueZeroNaN = NaN, 0 = −0SameValueNaN = NaN, 0 ≠ −0== !==== !==indexOflastIndexOfswitch casesincludesMap keysSet valuesObject.isReact state anddependency arrays
Four algorithms, and the operators and built-ins that use each one. Only == converts types.

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 display

This 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 == 1 is true, but 1n === 1 is false, 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) is false. Compare their timestamps instead: a.getTime() === b.getTime().

Which Comparison to Use

You want to checkUse
Almost anything===
Missing value (null or undefined)value == null
Is it NaN?Number.isNaN(value)
Exact identity, including -0Object.is(a, b)
Is this value in an array (incl. NaN)?array.includes(value)
Same contents in two objectsCompare fields, or a deep-equality helper
Strings that look the same in different languagesa.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.

โจทย์ที่ 1

An Empty Array

console.log([] == false, Boolean([]));
ดูคำตอบ

true true

  • [] == false converts false to 0, then [] to '' and then to 0, so 0 == 0.
  • Boolean([]) is true: every object is truthy.
  • So an empty array equals false and is still truthy in an if.

โจทย์ที่ 2

`null` and Zero

console.log(null == 0, null >= 0, undefined == 0, undefined >= 0);
ดูคำตอบ

false true false false

  • == has a special rule: null only equals null and undefined.
  • Relational operators convert to numbers instead: null becomes 0, so 0 >= 0 is true.
  • undefined becomes NaN, and every comparison with NaN is false.

โจทย์ที่ 3

How Big Is the Set?

const values = new Set([1, '1', 1.0, NaN, NaN, 0, -0]);
console.log(values.size);
ดูคำตอบ

4: 1, '1', NaN and 0

  • 1.0 is the same number as 1.
  • SameValueZero treats NaN as equal to itself and 0 as equal to -0.
  • No algorithm except == treats the string '1' as equal to the number 1.

โจทย์ที่ 4

Three Ways to Look for NaN

const list = [NaN];
console.log(list.indexOf(NaN), list.includes(NaN), list.findIndex(Number.isNaN));
ดูคำตอบ

-1 true 0

  • indexOf uses strict equality, which never matches NaN.
  • includes uses SameValueZero, which does.
  • findIndex uses whatever test you give it.

โจทย์ที่ 5

Wrapper Objects

const a = 'hi';
const b = new String('hi');
console.log(a === 'hi', b === 'hi', b == 'hi', typeof b);
ดูคำตอบ

true false true object

  • new String creates 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 Number or new Boolean.

โจทย์ที่ 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));
ดูคำตอบ

false false

  • Objects are compared by identity, so two separate objects are never equal.
  • JSON.stringify keeps 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 for value == null.
  • === says NaN !== NaN and 0 === -0; Object.is corrects both, and includes, Map and Set find NaN.
  • Objects are compared by identity under every algorithm, so compare contents explicitly or use stable keys.
  • Use Number.isNaN, not the global isNaN, 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

แท็ก

คำถามที่พบบ่อย

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.