Skip to main content

Cloudsoft Zone Build, deploy and scale with practical guides to PHP, Node.js, React, DevOps and AI automation. Explore the guides

Cloudsoft Zone
JavaScriptDevelopmentTutorial

How to compare arrays in JavaScript correctly

Compare JavaScript arrays shallowly, deeply or in any order with tested functions, avoid the JSON.stringify, toString and Set shortcuts that lie, and see how fast each method is.

Cloudsoft Zone

Updated 9 min read

Terminal output of the article's benchmark on Node.js 24.19.0: for loop, every, JSON.stringify and util.isDeepStrictEqual timings for numbers and nested objects
Image: Cloudsoft Zone
In this article

Two JavaScript arrays with the same contents are not equal under ===, because === compares object identity, not contents. To compare arrays you have to decide what "equal" means for your data: same values in the same order, the same values in any order, or the same nested structure. This guide gives a tested function for each case, shows which popular shortcuts give wrong answers, and measures how fast each approach is.

Tested with Node.js 24.19.0 (V8 13.6). Every result shown in comments was printed by running the code; the browser-compatible functions also work in current Chrome, Firefox and Safari, and the Set methods need Node.js 22 or a 2024-or-later browser.

Why === and == do not work

JavaScript
const a = [1, 2, 3];
const b = [1, 2, 3];
console.log(a === b); // false: two different array objects
console.log(a === a); // true: the same object

Arrays are objects, and object comparison checks whether both sides are the same object in memory. That is exactly what you want when you check whether a value has changed by reference (React's dependency arrays and memo rely on it), but it does not tell you whether two separate arrays hold the same data.

Choose the comparison you actually need

Most bugs in array comparison come from using an ordered check for unordered data, or a check that ignores duplicates when duplicates matter. Decide first:

Decision tree for comparing JavaScript arrays: ordered shallow or deep comparison, unordered with or without duplicates, records by id, and Set difference methods
Decide whether order, duplicates and nesting matter before choosing how to compare two arrays.

Same values, same order (shallow)

A plain loop is the most correct and the fastest option. It uses Object.is by default, so NaN equals NaN:

JavaScript
// Shallow, ordered: same length and same element at every index (holes count as undefined)
export function shallowEqual(a, b, same = Object.is) {
  if (a === b) return true;
  if (a.length !== b.length) return false;
  for (let i = 0; i < a.length; i++) {
    if (!same(a[i], b[i])) return false;
  }
  return true;
}
JavaScript
shallowEqual([1, 2, 3], [1, 2, 3]); // true
shallowEqual([NaN], [NaN]);         // true  (=== would say false)
shallowEqual([0], [-0]);            // false (pass (x, y) => x === y to treat them as equal)
shallowEqual([1, , 3], [1, 2, 3]);  // false (every() says true)

Why a for loop rather than every()? Array methods such as every() skip holes in sparse arrays, so a.every((v, i) => v === b[i]) returns true for [1, , 3] and [1, 2, 3]. The loop reads index 1 as undefined and correctly returns false. Sparse arrays are rare, but they appear after delete arr[i] or new Array(n).

Same values, any order

When duplicates count

For "same items, order irrelevant", count occurrences. This is O(n) and does not change either array:

JavaScript
// Unordered, duplicates matter (multiset): [1, 1, 2] is not [1, 2, 2]
export function sameMembers(a, b) {
  if (a.length !== b.length) return false;
  const counts = new Map();
  for (const x of a) counts.set(x, (counts.get(x) ?? 0) + 1);
  for (const x of b) {
    const n = counts.get(x);
    if (!n) return false;
    counts.set(x, n - 1);
  }
  return true;
}

Sorting copies and comparing them also works, but beware of the default sort: it compares values as strings.

JavaScript
[10, 9, 1].toSorted();                 // [1, 10, 9]  (string order!)
[10, 9, 1].toSorted((x, y) => x - y);  // [1, 9, 10]

When only distinct values matter

Comparing Set sizes alone is a common mistake: [1, 2, 3] and [4, 5, 6] both give a set of size three. Check membership as well. The Set methods added in ES2025 make this, and finding the differences, one line each:

JavaScript
const before = new Set(['a', 'b', 'c']);
const after = new Set(['b', 'c', 'd']);

const sameValues = before.size === after.size && before.isSubsetOf(after); // false
const added = [...after.difference(before)];             // ['d']
const removed = [...before.difference(after)];           // ['a']
const changed = [...before.symmetricDifference(after)];  // ['a', 'd']

Sets compare values with SameValueZero, so objects inside a Set are matched by identity, not by contents.

Deep comparison: nested arrays and objects

When elements are objects or arrays, compare recursively. This helper handles arrays, plain objects, Date, and Map and Set with primitive keys, and it survives circular references. It also requires both sides to have the same prototype, so a class instance never equals a plain object:

JavaScript
// Deep, ordered: arrays, plain objects, Dates, Maps and Sets of primitives
export function deepEqual(a, b, seen = new WeakMap()) {
  if (Object.is(a, b)) return true;
  if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false;
  if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false;
  if (seen.get(a) === b) return true; // already comparing this pair (cycles)
  seen.set(a, b);

  if (a instanceof Date) return Object.is(a.getTime(), b.getTime());
  if (a instanceof Map || a instanceof Set) {
    if (a.size !== b.size) return false;
    for (const [k, v] of a.entries()) {
      if (!b.has(k)) return false;
      if (a instanceof Map && !deepEqual(v, b.get(k), seen)) return false;
    }
    return true;
  }
  if (Array.isArray(a)) {
    if (a.length !== b.length) return false;
    for (let i = 0; i < a.length; i++) {
      if (!deepEqual(a[i], b[i], seen)) return false;
    }
    return true;
  }
  const keysA = Object.keys(a);
  if (keysA.length !== Object.keys(b).length) return false;
  for (const key of keysA) {
    if (!Object.hasOwn(b, key) || !deepEqual(a[key], b[key], seen)) return false;
  }
  return true;
}

On Node.js you do not need to write this yourself. util.isDeepStrictEqual uses the same rules as assert.deepStrictEqual, which you should use in tests because it prints a diff on failure (see testing a Node.js API with node:test):

JavaScript
import { isDeepStrictEqual } from 'node:util';
import assert from 'node:assert/strict';

isDeepStrictEqual([{ id: 1 }, [NaN]], [{ id: 1 }, [NaN]]); // true
isDeepStrictEqual([1], ['1']);                             // false: strict types

// In tests, assert gives you a readable diff when it fails
assert.deepStrictEqual(actualRows, expectedRows);

In browser code, either keep a small helper like the one above or use a maintained library such as Lodash's isEqual; do not ship a large dependency for one comparison.

Comparing arrays of records by id

When you sync a list from an API or a form, "equal" usually means "same rows with the same values, in any order". Index one side by id and compare pairs:

JavaScript
import { isDeepStrictEqual } from 'node:util';

const oldRows = [{ id: 1, qty: 2 }, { id: 2, qty: 1 }];
const newRows = [{ id: 2, qty: 3 }, { id: 3, qty: 1 }, { id: 1, qty: 2 }];

const oldById = new Map(oldRows.map((row) => [row.id, row]));
const report = newRows.map((row) => {
  const prev = oldById.get(row.id);
  if (!prev) return `${row.id}: added`;
  return isDeepStrictEqual(prev, row) ? `${row.id}: same` : `${row.id}: changed`;
});
// ['2: changed', '3: added', '1: same']
const newIds = new Set(newRows.map((row) => row.id));
const removed = oldRows.filter((row) => !newIds.has(row.id)); // []

This tells you what to insert, update and delete, which is usually what you needed to know in the first place. It is the pattern behind most list-diffing in admin screens and React tables; if those screens are slow or re-render on every keystroke, our React and Next.js front-end team can profile and fix them.

Shortcuts that give wrong answers

The two shortcuts most often suggested, JSON.stringify and toString(), convert both arrays to strings and compare those. They work for short arrays of plain numbers and strings, and fail silently for everything else:

Table of eight shortcut comparisons, such as JSON.stringify of undefined versus null and Set sizes of different arrays, all wrongly returning true in Node.js 24
Eight results printed by our test script: each shortcut called two different arrays equal.
  • JSON.stringify turns undefined, NaN and Infinity into null, serialises Map and Set as {}, throws on BigInt ("Do not know how to serialize a BigInt") and circular references, and depends on object key order: {a: 1, b: 2} and {b: 2, a: 1} compare as different.
  • toString() flattens nested arrays and does not distinguish numbers from strings: [1, 2] equals ["1,2"].

structuredClone pitfalls when comparing copies

A common test is "clone the data, change the clone, compare with the original". structuredClone is the right way to deep-copy data, but it does not preserve classes, and it cannot clone functions:

JavaScript
class Money {
  constructor(cents) { this.cents = cents; }
  format() { return (this.cents / 100).toFixed(2); }
}
const original = [new Money(4900), new Date(0), new Map([['k', 1]])];
const copy = structuredClone(original);

copy === original;                            // false
Object.getPrototypeOf(copy[0]) === Object.prototype; // true: the class is gone
isDeepStrictEqual(original, copy);            // false: prototypes differ
structuredClone([() => 1]);
// DataCloneError: () => 1 could not be cloned.

So a strict deep comparison of an original against its clone fails whenever the array holds class instances, even though every field matches. Compare the data you care about (for example copy[0].cents), or convert to plain objects first. Our article on structuredClone vs JSON vs spread covers copying in detail.

Performance

We timed each approach on Node.js 24.19.0 on an Apple M2, averaging 300 runs after a warm-up. Treat the numbers as relative, not absolute:

TestMethodTime per comparison
100,000 equal numbersfor loop (shallowEqual)0.08 ms
100,000 equal numbersutil.isDeepStrictEqual0.20 ms
100,000 equal numbersevery()0.57 ms
100,000 equal numbersJSON.stringify both1.76 ms
10,000 objects with nested arraysJSON.stringify both2.04 ms
10,000 objects with nested arraysutil.isDeepStrictEqual2.47 ms
10,000 objects with nested arraysdeepEqual (above)2.61 ms
100,000 numbers, first element differsfor loopunder 0.001 ms
100,000 numbers, first element differsJSON.stringify both1.89 ms

Two lessons: the plain loop wins for flat arrays and stops at the first difference, while JSON.stringify always serialises both arrays in full. For nested objects, JSON.stringify was slightly faster in our run, but it is the method with the wrong answers listed above, so the small gain is rarely worth it. If comparisons show up in a profile, the better fix is usually to avoid them: keep a version number or an immutable reference that changes when the data changes.

Strings: case and locale

For arrays of user-facing text, "the same" often means "the same ignoring case or accents". Pass a comparer to shallowEqual instead of changing the data:

JavaScript
const collator = new Intl.Collator('en', { sensitivity: 'base' });
const sameText = (x, y) => collator.compare(x, y) === 0;

shallowEqual(['Cafe', 'Tea'], ['café', 'tea'], sameText); // true
shallowEqual(['Cafe', 'Tea'], ['café', 'tea']);           // false

An Intl.Collator with sensitivity: 'base' treats "Cafe" and "café" as equal, which is what a search or duplicate check usually wants, while keeping the original strings untouched for display.

Common mistakes checklist

  • Comparing with == or === and expecting a content check.
  • Forgetting the length check, so [1, 2] "equals" [1, 2, 3] when you only loop over the first array.
  • Sorting the original arrays in place to compare them; use toSorted() or copy first, and pass a numeric comparator.
  • Using an ordered check for data whose order is not meaningful, such as tags or permissions.
  • Comparing floating-point results with equality instead of a tolerance.

When not to compare arrays at all

  • Change detection in UI state: use immutable updates and compare references, as React does.
  • Large server data: compare a hash or an updated_at column in the database instead of downloading both lists.
  • Floating-point results: compare with a tolerance, not equality.

Cloudsoft Zone builds and tunes React and Next.js front ends, including state handling and rendering performance. See our React and Next.js front-end development service.

Frequently asked questions

  • Why is [1, 2, 3] === [1, 2, 3] false in JavaScript?

    Arrays are objects, and === compares object identity. Two array literals create two different objects, so the result is false even though the contents match. Compare the elements instead.

  • Is JSON.stringify a good way to compare arrays?

    Only for small arrays of plain strings and finite numbers. It treats undefined and NaN as null, ignores Map and Set contents, depends on object key order, throws on BigInt and circular data, and always processes both arrays in full.

  • How do I check if two arrays have the same elements in any order?

    If duplicates matter, count each value in a Map and check the counts match. If only distinct values matter, compare new Set(a) and new Set(b) by size and isSubsetOf. Comparing set sizes alone is wrong.

  • What is the built-in deep equality check in Node.js?

    util.isDeepStrictEqual(a, b) returns a boolean, and assert.deepStrictEqual(a, b) throws with a diff, which is better in tests. Browsers have no built-in equivalent.

Advertise on CloudSoftZone

Your next customer could start right here.

Introduce your tool, product or business to readers exploring technology.

Advertise with us

Explore placements and request a private quote

Sources

  1. Equality comparisons and sameness — MDN Web Docs
  2. Array.prototype.every() — MDN Web Docs
  3. Set.prototype.difference() — MDN Web Docs
  4. JSON.stringify() — MDN Web Docs
  5. structuredClone() — MDN Web Docs
  6. util.isDeepStrictEqual — Node.js
  7. assert.deepStrictEqual — Node.js
  8. ECMAScript 2025 Language Specification — Ecma International TC39

Written with substantial AI assistance. Facts checked against cited sources. Editorial policy.

Comments

No comments yet. Be the first to join the conversation.

Leave a comment

Every comment is read by a moderator before it appears. Your email address is never published; it is used only to contact you about this comment.

Never published. Used only to contact you about this comment.

Loading the security check…

Comments are moderated before publication.