Array Equality and Duplicate Detection
Three related questions on arrays: are these two arrays equal in content, does this array have any duplicates at all, and how do I dedupe a list of objects by some key. Each one has a clean linear-time answer once you know which JS collection to lean on (`Set` for primitives, `Map` for keyed objects). The naive `O(n^2)` versions are fine for tiny arrays but show up in code reviews on anything bigger.
317 views
5
// Same length AND same items in the same order.
const arraysEqual = (a, b) => {
if (a.length !== b.length) return false;
return a.every((v, i) => v === b[i]);
};
console.log(arraysEqual([1, 2, 3], [1, 2, 3])); // true
console.log(arraysEqual([1, 2, 3], [3, 2, 1])); // false (order matters)
console.log(arraysEqual([1, 2], [1, 2, 3])); // false (length differs)
// 'Equal regardless of order': sort copies first, then compare.
const arraysEqualUnordered = (a, b) => {
if (a.length !== b.length) return false;
const sa = [...a].sort();
const sb = [...b].sort();
return sa.every((v, i) => v === sb[i]);
};
console.log(arraysEqualUnordered([1, 2, 3], [3, 2, 1])); // true
console.log(arraysEqualUnordered([1, 2, 2], [2, 1, 2])); // true (multiset equal)
// Caveat: object elements compare by REFERENCE, not by content.
console.log(arraysEqual([{ id: 1 }], [{ id: 1 }])); // falseThe shallow ordered check is length then every with strict equality. Strict equality (===) gets you NaN !== NaN, which is rarely what you want; swap to Object.is(v, b[i]) if NaN equality matters in your data. For unordered comparison, sort copies of both inputs first (do not sort the originals) and compare position-by-position. The big watch-out is object elements: [{id:1}] vs another [{id:1}] is unequal because the inner objects are different references. For structural object equality at the leaf level, swap to JSON.stringify comparison or js-object-deep-equal.
// Set size differs from array length iff there is at least one duplicate.
const hasDuplicates = (arr) => new Set(arr).size !== arr.length;
console.log(hasDuplicates([1, 2, 3, 4])); // false
console.log(hasDuplicates([1, 2, 2, 4])); // true
console.log(hasDuplicates(['a', 'b', 'a'])); // true
// Find which values duplicate.
const findDuplicates = (arr) => {
const seen = new Set();
const dupes = new Set();
for (const v of arr) {
if (seen.has(v)) dupes.add(v);
else seen.add(v);
}
return [...dupes];
};
console.log(findDuplicates([1, 2, 3, 2, 4, 3, 5])); // [2, 3]
// Beware: Set uses SameValueZero, so it groups objects by reference, not
// by content. To find duplicate objects by a key, see accordion 3.new Set(arr).size !== arr.length is the cleanest "does this array have duplicates?" check. The Set constructor walks the input once and skips repeats, so a smaller size means at least one repeat existed. To get the actual duplicate VALUES, walk the array yourself and track two sets: one for items already seen, one for items already flagged as duplicates. Both forms are linear time. For non-primitive items (objects), Set compares by reference, so two structurally-equal but differently-allocated objects are NOT considered duplicates by this method.
// Map keyed by a deduping field. The Map stores the FIRST occurrence; later
// duplicates are skipped because the key is already present.
const uniqueBy = (arr, keyFn) => {
const map = new Map();
for (const item of arr) {
const key = keyFn(item);
if (!map.has(key)) map.set(key, item);
}
return [...map.values()];
};
const users = [
{ id: 1, name: 'Ada' },
{ id: 2, name: 'Bob' },
{ id: 1, name: 'Ada (older record)' },
{ id: 3, name: 'Eve' }
];
console.log(uniqueBy(users, (u) => u.id));
// [{id:1, name:'Ada'}, {id:2, name:'Bob'}, {id:3, name:'Eve'}]
// To keep the LATEST record per key, swap the condition:
const uniqueByLatest = (arr, keyFn) => {
const map = new Map();
for (const item of arr) map.set(keyFn(item), item); // overwrite
return [...map.values()];
};
console.log(uniqueByLatest(users, (u) => u.id));
// [{id:1, name:'Ada (older record)'}, {id:2, name:'Bob'}, {id:3, name:'Eve'}]When the items are objects, you cannot drop them into a Set and expect dedup by content. The Map keyed by a function-extracted value is the canonical answer: walk the array, keep the first (or last) occurrence per key, then return the values. Insertion order is preserved by both Map and Set, so the output keeps the original order of first appearance. For dedup by multiple fields, build a composite key string (u => ${u.tenant}:${u.id}``); for primitives, fall back to [...new Set(arr)].
