Equality

Mental model: === asks "same type and same value". == asks the same question after converting, which is why it needs a rulebook.

Level: beginner · about 11 minutes

console.log(1 === '1');          // false  different types, no conversion
console.log(1 == '1');           // true   the string is converted to a number
console.log(null === undefined); // false  two distinct values
console.log(null == undefined);  // true   a hard-coded special case

Same four values, two operators, four different answers.

=== is strict equality: if the types differ the answer is false, full stop. == is loose equality: when the types differ it converts first, following a fixed sequence of steps. Neither is broken. One of them just does more work on your behalf than you can usually keep track of.

The == algorithm, in order

  1. Same type? Use === If both operands are already the same type, == and === agree completely. This is most real comparisons.
  2. null and undefined are equal to each other and nothing else This is a special case written directly into the spec, not a conversion. It is also the one genuinely useful thing == does.
  3. Number against string? Convert the string with ToNumber The string side moves to the number world, using the same conversion as unary +.
  4. Boolean on either side? Convert it with ToNumber and start over true becomes 1 and false becomes 0. This is why comparing anything to a boolean with == is a trap.
  5. Object against primitive? ToPrimitive the object and start over This is where arrays start comparing equal to numbers and strings, using the coercion rules from lesson 3.2.
aba == ba === b
0'0'truefalse
0''truefalse
0falsetruefalse
''falsetruefalse
'1'1truefalse
nullundefinedtruefalse
null0falsefalse
nullfalsefalsefalse
NaNNaNfalsefalse
[]falsetruefalse
[0]falsetruefalse
{}{}falsefalse

The rule

function greet(name) {
  if (name == null) return 'hello, stranger'; // catches null AND undefined
  return `hello, ${name}`;
}

console.log(greet());        // 'hello, stranger'
console.log(greet(null));    // 'hello, stranger'
console.log(greet(''));      // 'hello, ' an empty name is still a name
console.log(greet('Ada'));   // 'hello, Ada'

NaN and -0: where === itself is not enough

console.log(NaN === NaN);         // false  NaN is not equal to itself
console.log(Number.isNaN(NaN));   // true   the honest test
console.log(Object.is(NaN, NaN)); // true
console.log([NaN].includes(NaN)); // true   includes uses SameValueZero
console.log([NaN].indexOf(NaN));  // -1     indexOf uses ===
console.log(0 === -0);         // true   === cannot tell them apart
console.log(Object.is(0, -0)); // false  Object.is can
console.log(1 / 0, 1 / -0);    // Infinity -Infinity  the difference is real
console.log(Math.sign(-0));    // -0
ComparisonNaN vs NaN0 vs -0Use it for
==falsetruenothing, except x == null
===falsetrueeverything, by default
Object.istruefalseexact identity, cache keys, change detection
SameValueZero (includes, Map keys)truetruemembership tests

Objects compare by identity, never by content

const a = { id: 1 };
const b = { id: 1 };
const c = a;

console.log(a === b); // false  two separate objects
console.log(a === c); // true   the same object, two names
console.log([1, 2] === [1, 2]);                   // false
console.log(JSON.stringify([1, 2]) === '[1,2]');  // true, but fragile
function deepEqual(a, b) {
  if (Object.is(a, b)) return true;
  if (typeof a !== 'object' || typeof b !== 'object' || !a || !b) return false;
  if (Array.isArray(a) !== Array.isArray(b)) return false;
  const keys = Object.keys(a);
  if (keys.length !== Object.keys(b).length) return false;
  return keys.every((k) => deepEqual(a[k], b[k]));
}

console.log(deepEqual({ x: [1, 2] }, { x: [1, 2] })); // true
console.log(deepEqual({ x: 1 }, { x: 1, y: 2 }));     // false

Content equality is something you write, not something you find.

That version handles the common cases in ten lines. The exercise below asks you to close the gaps it leaves: a missing key whose value is undefined, and objects with the same key count but different keys.

console.log([] == false, [] === false, [0] == false, '0' == false);

[] == false: the boolean becomes 0, the array becomes '', '' becomes 0, so true. [] === false compares an object to a boolean, so false immediately. [0] == false: [0] becomes '0', which becomes 0, so true. '0' == false: false becomes 0 and '0' becomes 0, so true.

Try it yourself

Equality grid

const pairs = [
  [0, '0'], [0, ''], [0, false], ['', false],
  [null, undefined], [null, 0], [NaN, NaN], [[], false], [0, -0],
];

for (const [a, b] of pairs) {
  const label = `${JSON.stringify(a)} vs ${JSON.stringify(b)}`;
  /* eslint-disable-next-line eqeqeq */
  console.log(label.padEnd(22), '==', a == b, '| ===', a === b, '| Object.is', Object.is(a, b));
}

Add rows. Try [1] == 1, ' ' == 0, new String('a') == 'a', and anything you think you can predict.

Exercises

Rebuild Object.is

Write sameValue(a, b) that behaves exactly like Object.is without calling it. Two NaNs are the same value. 0 and -0 are not. Everything else follows ===.

Write deepEqual properly

Write deepEqual(a, b) comparing structure and content. Primitives use Object.is semantics, so two NaNs match and 0 does not match -0. Objects match when they have the same own keys and every value matches recursively. An array never matches a plain object. Do not use JSON.stringify.

Check yourself

What does this log?
false true — == has a hard-coded rule that null is only loosely equal to undefined, so null == 0 is false. Relational operators have no such rule: they run ToNumber(null), which is 0, so null >= 0 is true. Two operators, two different rulebooks.
Which comparison reports that NaN equals NaN?
Object.is(NaN, NaN) — == and === both say false, because NaN is specified as not equal to any value including itself. Object.is uses SameValue, which treats the NaNs as the same value. Number.isNaN(x) is the usual way to test a single value.
What is the one == comparison worth keeping in production code?
x == null to catch null and undefined — x == null is true for exactly null and undefined, and false for 0, '', false and NaN. It is precise and short. The other three all depend on coercion steps that make the code harder to read than the explicit version.

Common mistakes

  • Using == with a boolean, where '1' == true is true but 'a' == true is false.
  • Comparing objects or arrays with === and expecting content comparison.
  • Reaching for JSON.stringify(a) === JSON.stringify(b), which depends on key order and drops undefined.

Takeaways

  • === compares type and value with no conversion. Default to it.
  • == converts first, and the result is not transitive, so it cannot be reasoned about locally.
  • x == null is the one allowance: exactly null or undefined.
  • Object.is differs from === on precisely two inputs: NaN and -0.
  • Objects compare by identity, so content equality is code you write.