Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

JavaScript (ES2026)  Module 2 – Values, types and operators

Equality: ==, === and Object.is

What == converts before it compares, why === is the safe default, how Object.is and SameValueZero treat NaN and -0, and how to compare objects by content.

  • Beginner
  • 20 minutes
  • Examples run with Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Use === by default and explain each conversion that == makes before it compares
  • Choose between ===, Object.is and SameValueZero, the comparison behind includes, Map and Set
  • Explain why value == null is the one loose comparison many teams allow
  • Compare arrays and objects by their contents with a function written for the job

Before you start

What you’ll learn · Before you start

What you will learn

  • Use === by default and explain each conversion that == makes before it compares
  • Choose between ===, Object.is and SameValueZero, the comparison behind includes, Map and Set
  • Explain why value == null is the one loose comparison many teams allow
  • Compare arrays and objects by their contents with a function written for the job

Before you start

On this page

JavaScript has four ways to ask whether two values are the same, and they disagree on purpose. ===, strict equality, compares without converting anything, so values of different types are never equal. ==, loose equality, first converts its operands by a fixed list of rules, which is why '' == 0 and [] == false are both true. Object.is is strict equality with two corrections: NaN equals NaN, and 0 differs from -0. SameValueZero, the comparison behind includes, Map and Set, also counts NaN as equal to itself but treats 0 and -0 as one value. All four compare objects by reference: an object is equal only to itself, however alike another object looks. Use === by default, includes or Object.is when NaN can appear, and a function written for the job when you need to compare contents. This lesson shows each one at work, with the rules of == step by step.

Strict equality: ===

=== asks whether both sides have the same type and the same value. It never converts:

What === says about seven pairs JavaScript · strict.mjs
// === never converts: values of different types are never equal, and an object is equal only to itself.
const order = { id: 7 };
const sameOrder = order;
const pairs = [
  ["'1'", '1', '1', 1],
  ['1n', 1n, '1', 1],
  ['NaN', NaN, 'NaN', NaN],
  ['0', 0, '-0', -0],
  ['order', order, 'sameOrder', sameOrder],
  ['order', order, '{ id: 7 }', { id: 7 }],
  ['[1, 2]', [1, 2], '[1, 2]', [1, 2]],
];
for (const [left, a, right, b] of pairs) {
  console.log(`${left} === ${right}`.padEnd(26), a === b);
}

Output

'1' === 1                  false
1n === 1                   false
NaN === NaN                false
0 === -0                   true
order === sameOrder        true
order === { id: 7 }        false
[1, 2] === [1, 2]          false

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node strict.mjs

  • Different types are never equal: the string '1' is not the number 1, and the BigInt 1n is not the number 1 either.
  • Numbers compare by value, with two exceptions: NaN is not equal to anything, itself included, and 0 equals -0.
  • Objects compare by reference, and arrays and functions are objects. order === sameOrder is true because both names point to one object; a second object with the same contents, { id: 7 }, is a different object.

!== gives the opposite answer. A switch statement also compares with strict equality, so case 1: never matches the string '1'.

Loose equality: == converts first

== asks a series of questions, in a fixed order, and the first one that applies decides what happens next. Several of them convert one side and start the comparison again:

Seven steps of x == y in order: same type means ===; strings, booleans and objects are converted and compared again.1. Same type?yes: the answer of x === y2. One null, the other undefined?yes: true3. A number or BigInt against a string?yes: convert the string, then start again4. A boolean on either side?yes: turn it into 1 or 0, then start again5. An object against a primitive?yes: turn the object into a primitive,then start again6. A BigInt against a number?yes: compare their values7. Anything else: false(null or undefined against any other value)nononononono

The questions x == y asks, in order

Text description of the diagram

Seven boxes, one above the other, joined by arrows labelled "no". The first question that is answered "yes" decides the result; "start again" means the comparison restarts with the converted value.

  1. Are x and y of the same type? If so, the answer is that of x === y.
  2. Is one of them null and the other undefined? If so, the answer is true.
  3. Is one a number or a BigInt and the other a string? If so, the string is converted to a number (or a BigInt) and the comparison starts again.
  4. Is either one a boolean? If so, it becomes 1 for true or 0 for false, and the comparison starts again.
  5. Is one an object and the other a primitive? If so, the object is turned into a primitive, and the comparison starts again.
  6. Is one a BigInt and the other a number? If so, their mathematical values are compared.
  7. Anything else gives false. This is where null or undefined compared with any other value ends up, which is why null == 0 and null == false are both false.

Follow [] == false through the steps. The types differ, neither side is null or undefined, and there is no number-and-string pair yet. There is a boolean, so false becomes 0, giving [] == 0. Now an object meets a primitive, so [] becomes the empty string, giving '' == 0. A string meets a number, so '' becomes 0, and 0 == 0 is true. The same steps take '0' == false to 0 == 0, so it is true, although '0' is truthy. And null == false becomes null == 0 at the boolean step, which then matches nothing until the last step, so it is false: null is loosely equal only to undefined.

Here is == for eight values that all look empty in some way:

== between eight empty-looking values JavaScript · equality-grid.mjs
/* eslint-disable eqeqeq -- this example shows what == does */
// a == b for eight values that look "empty": ✓ where == says true.
const values = [false, 0, '', '0', [], [0], null, undefined];
const labels = ['false', '0', "''", "'0'", '[]', '[0]', 'null', 'undef'];

console.log((''.padEnd(7) + labels.map((l) => l.padEnd(6)).join('')).trimEnd());
values.forEach((a, i) => {
  const cells = values.map((b) => (a == b ? '✓' : '·').padEnd(6));
  console.log((labels[i].padEnd(7) + cells.join('')).trimEnd());
});

Output

       false 0     ''    '0'   []    [0]   null  undef
false  ✓     ✓     ✓     ✓     ✓     ✓     ·     ·
0      ✓     ✓     ✓     ✓     ✓     ✓     ·     ·
''     ✓     ✓     ✓     ·     ✓     ·     ·     ·
'0'    ✓     ✓     ·     ✓     ·     ✓     ·     ·
[]     ✓     ✓     ✓     ·     ✓     ·     ·     ·
[0]    ✓     ✓     ·     ✓     ·     ✓     ·     ·
null   ·     ·     ·     ·     ·     ·     ✓     ✓
undef  ·     ·     ·     ·     ·     ·     ✓     ✓

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node equality-grid.mjs

Two things stand out. null and undefined are equal to each other and to nothing else in the grid. And == is not transitive, as equality in mathematics is: '' equals 0, and 0 equals '0', but '' does not equal '0', because two strings are compared as text without any conversion.

The relational operators make it stranger still:

null against 0 JavaScript · null-compare.mjs
/* eslint-disable eqeqeq -- the loose comparison is part of what this example shows */
// null against 0: the relational operators convert null to 0, the equality operators do not.
const nothing = null;
console.log('null == 0 ', nothing == 0);
console.log('null >  0 ', nothing > 0);
console.log('null >= 0 ', nothing >= 0);

Output

null == 0  false
null >  0  false
null >= 0  true

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node null-compare.mjs

<, >, <= and >= convert null to 0, while == never converts null at all. And >= is worked out as “not less than”: null becomes 0, 0 is not less than 0, so null >= 0 is true while null == 0 and null > 0 are false.

None of this is worth memorising to use. It is worth knowing well enough to recognise in code you read, and to explain why === is the default: it has one rule, while == has seven.

The one useful ==: value == null

One loose comparison is short, exact and common in real code:

value == null JavaScript · null-check.mjs
// value == null is true for null and undefined, and for nothing else.
const values = [null, undefined, 0, '', false, NaN, [], 'null'];
const labels = ['null', 'undefined', '0', "''", 'false', 'NaN', '[]', "'null'"];
values.forEach((value, i) => {
  console.log(labels[i].padEnd(10), value == null);
});

Output

null       true
undefined  true
0          false
''         false
false      false
NaN        false
[]         false
'null'     false

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node null-check.mjs

value == null is true for null and undefined and false for everything else, 0, '', false and NaN included, so it is a precise test for “missing”. ESLint’s eqeqeq rule has a setting for exactly this case, ["error", "always", { "null": "ignore" }], which requires === everywhere except in comparisons with null. Teams that prefer to spell both cases out write value === null || value === undefined. Either is fine, as long as a code base picks one.

Object.is and SameValueZero

Object.is and SameValueZero agree with === on everything except NaN and -0:

NaN, -0, includes, Map and Set JavaScript · sameness.mjs
// Object.is and SameValueZero differ from === only for NaN and -0.
const pairs = [
  ['NaN', NaN, 'NaN', NaN],
  ['0', 0, '-0', -0],
];
for (const [left, a, right, b] of pairs) {
  console.log(`${left}, ${right}:`.padEnd(10), '===', a === b, ' Object.is', Object.is(a, b), ' includes', [a].includes(b));
}

// indexOf compares with ===, so it can never find NaN; includes and findIndex can.
const readings = [12, NaN, 7];
const missing = readings[1];
console.log(readings.indexOf(missing), readings.includes(missing), readings.findIndex(Number.isNaN));

// Map and Set use SameValueZero: one NaN key, and -0 and 0 are the same key.
const seen = new Set([NaN, Number('x'), 0, -0]);
console.log(seen.size);
const prices = new Map([[-0, 'zero']]);
console.log(prices.get(0), Object.is([...prices.keys()][0], -0));

Output

NaN, NaN:  === false  Object.is true  includes true
0, -0:     === true  Object.is false  includes true
-1 true 1
2
zero false

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node sameness.mjs

== === Object.is SameValueZero
converts types first yes no no no
NaN and NaN not equal not equal equal equal
0 and -0 equal equal not equal equal
null and undefined equal not equal not equal not equal
used by ==, != ===, !==, indexOf, switch Object.is includes, Map and Set keys

What that means in practice:

  • indexOf can never find NaN, because it compares with ===. includes can, and findIndex(Number.isNaN) gives the position.
  • Map and Set keep a single NaN key, and they treat 0 and -0 as the same key. A -0 key is stored as 0, as the last line of the run shows.
  • -0 is rare but real. It comes from rounding a small negative number, as Math.round(-0.4) does, or from multiplying 0 by a negative number. String(-0) is '0', which hides it in text; Object.is(x, -0) is the clear way to test for it when its sign matters, as with a direction or a trend.

Comparing contents

=== answers “is this the same object?”, which is often not the question. To ask “do these hold the same values?”, you need a comparison written for the job:

Two arrays, the JSON shortcut and a real comparison JavaScript · by-content.mjs
// Two arrays with the same items are still two arrays.
const a = [1, 2, 3];
const b = [1, 2, 3];
console.log(a === b);

// Comparing JSON text looks like a shortcut, and it is wrong in four common ways.
const cases = [
  ['key order', { x: 1, y: 2 }, { y: 2, x: 1 }],
  ['NaN and null', { total: NaN }, { total: null }],
  ['undefined and missing', { note: undefined }, {}],
  ['-0 and 0', [-0], [0]],
];
for (const [what, left, right] of cases) {
  console.log(what.padEnd(22), JSON.stringify(left) === JSON.stringify(right));
}

// A comparison written for the job: same length, and Object.is for every item.
function sameItems(left, right) {
  return left.length === right.length && left.every((item, i) => Object.is(item, right[i]));
}
console.log(sameItems(a, b), sameItems([NaN], [NaN]), sameItems([1, 2], [2, 1]));

Output

false
key order              false
NaN and null           true
undefined and missing  true
-0 and 0               true
true true false

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node by-content.mjs

Comparing JSON.stringify output looks like a shortcut and fails in four common ways: the same contents with the keys in another order give different text, while NaN and null, undefined and a missing key, and -0 and 0 give the same text. A function that says exactly what “the same” means does better. For flat arrays, sameItems compares the lengths and then every item with Object.is; for objects, the exercise below compares keys and values one level deep.

For nested data, use a deep comparison that is already written and tested, rather than a new one. Node.js has one in its util module, an API of Node.js itself rather than of the language, so this example runs on Node.js only:

util.isDeepStrictEqual on Node.js JavaScript · node-deep-equal.mjs
// Node.js ships a deep comparison in its util module (a Node.js API, not part of the language).
import { isDeepStrictEqual } from 'node:util';

console.log(isDeepStrictEqual({ a: 1, b: [2, 3] }, { b: [2, 3], a: 1 }));
console.log(isDeepStrictEqual({ total: NaN }, { total: NaN }));
console.log(isDeepStrictEqual({ total: 1 }, { total: '1' }));
console.log(isDeepStrictEqual({ note: undefined }, {}));
console.log(isDeepStrictEqual(new Map([['k', 1]]), new Map([['k', 1]])));

Output

true
true
false
false
true

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node node-deep-equal.mjs

isDeepStrictEqual ignores the order of keys, treats NaN as equal to NaN, does not convert '1' to 1, tells a missing key from one set to undefined and compares the entries of a Map. The language itself has no deep equality, so code that runs in browsers takes one from a library.

JSON Diff & Compare See exactly which fields of two JSON documents differ when a content comparison says they are not equal. JavaScript & TypeScript Online Compiler Try your own pairs with ==, ===, Object.is and includes.

Exercise: one level deep

Exercise · Medium · JavaScript

Compare two objects or arrays one level deep

A form keeps the values a user loaded and the values on screen, and enables its Save button only when something changed. Write shallowEqual(a, b), which compares two values one level deep:

  • if Object.is(a, b) is true, they are equal (the same object, or the same primitive);
  • two arrays are equal when they have the same length and Object.is holds for the items at every index;
  • two plain objects are equal when they have the same own keys (as Object.keys lists them, in any order) and Object.is holds for the value under each key;
  • anything else is not equal: an array is never equal to an object, null is never equal to an object, and two different primitives are not equal.

Values nested inside are compared by reference, not by content: that is what "shallow" means. Return true or false.

shallowEqual({ a: 1, b: 2 }, { b: 2, a: 1 })  // true: key order does not matter
shallowEqual([NaN], [NaN])                    // true: Object.is(NaN, NaN)
shallowEqual([0], [-0])                       // false: Object.is(0, -0) is false
shallowEqual({ a: 1 }, { a: 1, b: undefined }) // false: b is an extra key

The starter code compares with ===, which is true only for the same object: run the sample tests to see what that misses.

Starter code · shallow.mjs

/**
 * True when a and b are equal one level deep: Object.is(a, b), or two arrays with Object.is-equal items at every
 * index, or two plain objects with the same keys and Object.is-equal values under each key.
 */
export function shallowEqual(a, b) {
  // Replace this line with your code.
  return a === b;
}
The sample tests · shallow.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { shallowEqual } from './shallow.mjs';

test('the same object, and equal primitives', () => {
  const form = { name: 'Asha' };
  assert.equal(shallowEqual(form, form), true);
  assert.equal(shallowEqual(5, 5), true);
  assert.equal(shallowEqual('a', 'a'), true);
  assert.equal(shallowEqual(5, 6), false);
  assert.equal(shallowEqual(null, null), true);
  assert.equal(shallowEqual(null, undefined), false);
});

test('arrays with the same items in the same order', () => {
  assert.equal(shallowEqual([1, 2, 3], [1, 2, 3]), true);
  assert.equal(shallowEqual([1, 2], [2, 1]), false);
  assert.equal(shallowEqual([1, 2], [1, 2, 3]), false);
  assert.equal(shallowEqual([], []), true);
});

test('objects with the same keys, in any order', () => {
  assert.equal(shallowEqual({ a: 1, b: 2 }, { b: 2, a: 1 }), true);
  assert.equal(shallowEqual({ a: 1, b: 2 }, { a: 1, b: 3 }), false);
  assert.equal(shallowEqual({}, {}), true);
});

test('an extra or a different key makes them differ', () => {
  assert.equal(shallowEqual({ a: 1 }, { a: 1, b: undefined }), false);
  assert.equal(shallowEqual({ a: 1, b: undefined }, { a: 1 }), false);
  assert.equal(shallowEqual({ a: 1, b: 2 }, { a: 1, c: 2 }), false);
});

test('values are compared with Object.is', () => {
  assert.equal(shallowEqual([NaN], [NaN]), true);
  assert.equal(shallowEqual({ total: NaN }, { total: NaN }), true);
  assert.equal(shallowEqual([0], [-0]), false);
  assert.equal(shallowEqual({ x: '1' }, { x: 1 }), false);
});

test('nested values are compared by reference', () => {
  const address = { city: 'Pune' };
  assert.equal(shallowEqual({ address }, { address }), true);
  assert.equal(shallowEqual({ address: { city: 'Pune' } }, { address: { city: 'Pune' } }), false);
});

test('different kinds of value are never equal', () => {
  assert.equal(shallowEqual([], {}), false);
  assert.equal(shallowEqual([1], { 0: 1 }), false);
  assert.equal(shallowEqual({ 0: 1 }, [1]), false);
  assert.equal(shallowEqual(null, {}), false);
  assert.equal(shallowEqual({}, null), false);
  assert.equal(shallowEqual('1', ['1']), false);
  assert.equal(shallowEqual({}, 0), false);
  assert.equal(shallowEqual('ab', { 0: 'a', 1: 'b' }), false);
  assert.equal(shallowEqual({ 0: 'a', 1: 'b' }, 'ab'), false);
});
A hint

Start with if (Object.is(a, b)) return true;. After that, both values must be objects that are not null, and Array.isArray must give the same answer for both. Then compare the number of keys from Object.keys, and check every key of a with Object.hasOwn(b, key) and Object.is(a[key], b[key]). Arrays need no special code after that: their keys are their indexes.

The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Self-check

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 Which of these comparisons give true? Choose every one.

    Choose every answer that is right.

    Show the answer to question 1

    Answer:

    • [] == false
    • '' == 0
    • null == undefined

    null and undefined are loosely equal to each other. '' becomes 0 against a number. In [] == false, false becomes 0, [] becomes '' and then 0. null equals nothing but undefined, NaN equals nothing at all, and two strings are compared as text, so '' and '0' differ.

  2. Question 2 of 6 readings may contain NaN. Which expression tells you correctly whether it does?

    Choose one answer.

    Show the answer to question 2

    Answer: readings.includes(NaN)

    includes compares with SameValueZero, which treats NaN as equal to NaN. indexOf, lastIndexOf and === all use strict equality, under which NaN is never equal to anything, so they never find it.

  3. Question 3 of 6 What does this code print?

    Read the code, then choose one answer.

    const seen = new Set([0, -0, NaN, NaN, '0']);
    console.log(seen.size);
    Show the answer to question 3

    Answer: 3

    A Set compares with SameValueZero: 0 and -0 are one value, and the two NaNs are one value. The string '0' is a different type, so it is a third entry.

  4. Question 4 of 6 null-compare.mjs compares null with 0 three ways. What does it print?

    What does this program print? Choose one answer.

    /* eslint-disable eqeqeq -- the loose comparison is part of what this example shows */
    // null against 0: the relational operators convert null to 0, the equality operators do not.
    const nothing = null;
    console.log('null == 0 ', nothing == 0);
    console.log('null >  0 ', nothing > 0);
    console.log('null >= 0 ', nothing >= 0);
    Show the answer to question 4

    Answer: it prints

    null == 0  false
    null >  0  false
    null >= 0  true

    == never converts null, which equals only undefined. The relational operators convert it to 0: 0 is not greater than 0, and >= means "not less than", so null >= 0 is true.

  5. Question 5 of 6 Which condition is true exactly when value is null or undefined?

    Choose one answer.

    Show the answer to question 5

    Answer: value == null

    value == null is true for null and undefined and for nothing else. !value is also true for 0, '', false and NaN; value === null misses undefined; and typeof gives 'object' for null.

  6. Question 6 of 6 JSON.stringify(a) === JSON.stringify(b) gives the wrong answer for which pairs? Choose every one.

    Choose every answer that is right.

    Show the answer to question 6

    Answer:

    • { total: NaN } and { total: null }
    • { x: 1, y: 2 } and { y: 2, x: 1 }
    • { note: undefined } and {}

    The first pair has the same contents but different text, so the check says "different". The second and third pairs differ, but NaN is written as null and undefined properties are dropped, so the check says "the same". The last two pairs are judged correctly.

Interview questions

What is the difference between == and ===? === compares without converting: if the two values have different types, the answer is false, and otherwise it compares the values, with objects compared by reference. == first converts by a list of rules: a string compared with a number becomes a number, a boolean becomes 1 or 0, an object becomes a primitive, and null and undefined are equal to each other and to nothing else. That is why '' == 0, '0' == false and [] == false are all true. Both agree that NaN is not equal to NaN and that 0 equals -0. Use === by default, because it has one rule; the one loose comparison many teams allow is value == null, which is true for null and undefined only.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    An import job reads 2 million orders from a JSON file and puts them into a Set to drop duplicates, but the Set ends up as large as the input. Two orders count as duplicates when their id and amount match. Explain why the Set kept everything, and design a deduplication that scales.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A 200,000-line JavaScript code base uses == in about 300 places. Option A: === everywhere. Option B: === everywhere except value == null. Choose one, defend it, and describe how you would migrate the 300 places without breaking production.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • === never converts: different types are never equal, NaN is never equal to itself, 0 equals -0, and objects are equal only to themselves.
  • == converts by a fixed series of steps: strings meeting numbers become numbers, booleans become 1 or 0, objects become primitives, and null equals only undefined. It is not even transitive.
  • null >= 0 is true while null == 0 is false: the relational operators convert null, equality does not.
  • value == null is a precise test for null or undefined, and ESLint’s eqeqeq rule can allow just that case.
  • Object.is differs from === only for NaN and -0; includes, Map and Set use SameValueZero, so they find NaN and treat -0 as 0, while indexOf cannot find NaN.
  • Compare contents with a function written for the job, never with JSON.stringify; use a tested deep comparison for nested data.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.