JAVASCRIPT / MAPS, SETS, SYMBOLS, AND PROXIES
Set fundamentals and deduplication
Build Sets from any iterable, predict which values count as duplicates under SameValueZero, and deduplicate arrays and object lists without quadratic scans.
What you will learn
- Deduplicate an array with [...new Set(arr)] while keeping first-occurrence order
- Predict duplicates with SameValueZero: NaN matches NaN, +0 and -0 share a slot
- Dedupe object lists through a Set of primitive keys, not the objects themselves
- Convert a Set back with [...set] or Array.from before using array methods
Understanding Set fundamentals and deduplication
A Set is a collection that refuses to store the same value twice. Every add first asks whether an equal value is already inside; if it is, the call is a no-op and size does not move. Values iterate in the order they were first accepted, which is why deduplicating through a Set preserves the position of the first occurrence and silently drops the later ones.
The comparison Set uses is SameValueZero: identical to === except that NaN counts as equal to itself and +0 and -0 land in the same slot. That is why new Set([NaN, NaN]).size is 1 even though NaN === NaN evaluates to false, and why a dedup written with indexOf keeps every NaN it meets, since indexOf compares with ===. For objects, SameValueZero falls back to reference identity: JavaScript has no built-in structural equality, so two separately written literals with the same fields are simply two different values.
Set lookups are backed by a hash-style index rather than a scan, so has and add cost roughly constant time no matter how many values are stored. That is the real argument for [...new Set(arr)]: a filter with indexOf re-scans the accumulated result for every element and is quadratic, while the Set version touches each element once. The price is that a Set is not an array — no index access, no length, no map — so you spread it or use Array.from when you need array methods again.
const raw = [3, 1, 3, NaN, NaN, 0, -0, '3', 1];
const unique = [...new Set(raw)];
console.log(unique.length);
console.log(unique.map(v => String(v) + ':' + typeof v).join(' '));
const seen = new Set(raw);
console.log(seen.has(NaN), seen.has(-0), seen.has('1'));
seen.add(3).add(99);
console.log(seen.size);
seen.delete(NaN);
console.log(seen.size, seen.has(NaN));A Set holds at most one copy of each value under SameValueZero equality in first-insertion order, and that single rule is what turns deduplication into a one-liner.
Worked examples
Deduplicating objects by a key
Shows why a Set of objects removes nothing, and how a Set of primitive ids fixes it.
const rows = [
{ id: 1, tag: 'a' },
{ id: 2, tag: 'b' },
{ id: 1, tag: 'a' }
];
console.log(new Set(rows).size);
const seenIds = new Set();
const firstPerId = [];
for (const row of rows) {
if (seenIds.has(row.id)) continue;
seenIds.add(row.id);
firstPerId.push(row);
}
console.log(firstPerId.length, seenIds.size);
console.log(firstPerId.map(r => r.tag).join(','));Example explained
Line 1new Set(rows).size is 3 because each object literal is a fresh reference and no two rows compare equal.
Line 2seenIds stores numbers instead of objects, so the id 1 in the third row is recognised as already seen.
Line 3The continue keeps the first row for each id, mirroring how Set dedup behaves for primitives.
Line 4has and add are near-constant time, so the loop stays linear in the number of rows.
Any iterable feeds the constructor
Demonstrates that the Set constructor consumes iterables, including strings and other Sets.
console.log(new Set('mississippi').size);
console.log([...new Set('mississippi')].join(''));
console.log(new Set('abc').size, new Set(['abc']).size);
console.log(new Set(new Set([1, 1, 2])).size);Example explained
Line 1Strings iterate character by character, so 'mississippi' collapses to the four distinct letters m, i, s, p.
Line 2join('') prints them in first-insertion order, which is the order the letters first appear in the word.
Line 3new Set('abc') holds three entries while new Set(['abc']) holds one: a bare string gets spread, an array of one string does not.
Line 4A Set is itself iterable, so passing one to the constructor produces a copy.
Order: first insertion wins
Shows that re-adding an existing value is inert, while delete followed by add moves the value to the end.
const s = new Set(['a', 'b', 'c']);
s.add('a');
console.log([...s].join(''), s.size);
s.delete('a');
s.add('a');
console.log([...s].join(''));
console.log([...new Set(['b', 'a', 'b'])].join(''));Example explained
Line 1s.add('a') does nothing because an equal value is present, so neither the order nor the size changes.
Line 2delete erases the insertion record, so the following add appends 'a' after 'b' and 'c'.
Line 3Dedup keeps the earliest copy: the first 'b' was accepted before 'a', so 'b' stays in front.
Important notes
Set.prototype.forEach calls back with (value, value, set); the value is passed twice only so the signature matches Map and Array, since a Set has no separate key.
add(-0) normalises to +0, so a Set built from [-0] iterates as 0 while has(-0) still returns true.
Common mistakes
Expecting content-based dedup: new Set([{id:1},{id:1}]).size is 2, so an object list passes through unchanged and the duplicates reach the UI or the database.
Passing a string where an array was meant: new Set('ab') holds 'a' and 'b', turning one intended entry into one entry per character.
Treating the result as an array: set.length is undefined so index loops never run, and set.map(...) throws TypeError: set.map is not a function.
Try it yourself
Change, predict, then run
In a browser console, dedupe ['Ann','bob','ANN','Bob','ann'] twice: once with [...new Set(list)], then case-insensitively by keeping a Set of lowercased names and pushing only the first spelling of each. Log both lengths and both arrays.
Open the JavaScript workspaceCheck your understanding
You dedupe an array of point objects with [...new Set(points)] and the length never changes, even though several points have identical x and y values. What explains this?
- Spreading the Set back into an array restores the duplicates that add had removed
- Set skips deduplication for any value whose typeof is 'object'
- Set compares objects by reference, so two literals with the same fields are two distinct values
- Set only removes duplicates that sit next to each other in the source iterable
Show answer
SameValueZero compares objects by identity, and every object literal creates a new reference, so nothing matches and nothing is dropped. Option 1 is tempting but false: a Set does dedupe objects, just by reference — push the same object twice and only one copy is kept, which is exactly why you need a primitive key instead.