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.
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:
// === 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
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
- Different types are never equal: the string
'1'is not the number 1, and the BigInt1nis not the number 1 either. - Numbers compare by value, with two exceptions:
NaNis not equal to anything, itself included, and0equals-0. - Objects compare by reference, and arrays and functions are objects.
order === sameOrderistruebecause 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:
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.
- Are x and y of the same type? If so, the answer is that of x === y.
- Is one of them null and the other undefined? If so, the answer is true.
- 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.
- Is either one a boolean? If so, it becomes 1 for true or 0 for false, and the comparison starts again.
- Is one an object and the other a primitive? If so, the object is turned into a primitive, and the comparison starts again.
- Is one a BigInt and the other a number? If so, their mathematical values are compared.
- 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:
/* 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
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
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:
/* 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
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
<, >, <= 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 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
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
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:
// 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
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
== |
=== |
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:
indexOfcan never findNaN, because it compares with===.includescan, andfindIndex(Number.isNaN)gives the position.MapandSetkeep a singleNaNkey, and they treat0and-0as the same key. A-0key is stored as 0, as the last line of the run shows.-0is rare but real. It comes from rounding a small negative number, asMath.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 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
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
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:
// 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.
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)istrue, they are equal (the same object, or the same primitive); - two arrays are equal when they have the same length and
Object.isholds for the items at every index; - two plain objects are equal when they have the same own keys (as
Object.keyslists them, in any order) andObject.isholds for the value under each key; - anything else is not equal: an array is never equal to an object,
nullis 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.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
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.
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.
-
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.
-
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.
Key takeaways
===never converts: different types are never equal,NaNis never equal to itself,0equals-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, andnullequals onlyundefined. It is not even transitive.null >= 0istruewhilenull == 0isfalse: the relational operators convertnull, equality does not.value == nullis a precise test fornullorundefined, and ESLint’seqeqeqrule can allow just that case.Object.isdiffers from===only forNaNand-0;includes,MapandSetuse SameValueZero, so they findNaNand treat-0as 0, whileindexOfcannot findNaN.- Compare contents with a function written for the job, never with
JSON.stringify; use a tested deep comparison for nested data.
References
- ECMAScript 2026 Language Specification: IsLooselyEqual (Ecma International)
- ECMAScript 2026 Language Specification: IsStrictlyEqual (Ecma International)
- ECMAScript 2026 Language Specification: SameValue (Ecma International)
- ECMAScript 2026 Language Specification: SameValueZero (Ecma International)
- ECMAScript 2026 Language Specification: Relational Operators (Ecma International)
- ECMAScript 2026 Language Specification: Array.prototype.includes (Ecma International)
- Equality comparisons and sameness (MDN Web Docs)
- eqeqeq: require the use of === and !== (ESLint)
- util.isDeepStrictEqual (Node.js v24) (OpenJS Foundation (Node.js))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress