You're asked to deduplicate. Implement uniqueArray(arr): return a new array that contains each value from arr exactly once, in the order each value first appeared. Don't mutate the input; don't reorder things.
This is the same job as wrapping an array with new Set(...) and reading the result back out. You can use a Set, or roll your own bookkeeping — either way the contract is what matters.
function uniqueArray(arr) {
// returns a new array containing the distinct values of `arr`,
// in the order each value first appeared.
}
uniqueArray([1, 2, 2, 3, 1, 4]); // [1, 2, 3, 4]
uniqueArray(['a', 'b', 'a', 'c', 'b']); // ['a', 'b', 'c']
uniqueArray([]); // []
uniqueArray([NaN, NaN, 1, NaN]); // [NaN, 1] — NaN counts as equal to NaN
uniqueArray([{ a: 1 }, { a: 1 }]); // both kept — objects compare by reference
SameValueZero — the same rule Set and Array.prototype.includes use. NaN equals NaN, and -0 equals 0.[], not undefined.You'll walk the array once, remember every value you've already emitted in a Set, and skip anything you've seen before.
You have a list of values. Some are repeats. You want the list with each value kept only the first time it shows up — the second, third, and tenth appearances disappear. The output stays in the same order as the input, just without the echoes. That's what dedupe-preserving-order means, and it's the core of building things like a "recent searches" list or a tag picker that doesn't let the same tag appear twice.
Imagine reading the array left to right and writing values into an output list, but with a doorman at the entrance holding a clipboard. Each value walks up to the door; the doorman checks the clipboard for that value. If it's not there, the doorman writes it down and lets it through. If it's already on the clipboard, the value is turned away. The clipboard is your Set; the output list is what made it past the door.
A reasonable first try: walk the array and, for each value, check whether it's already in the output before pushing it.
function uniqueArrayBroken(arr) {
const out = [];
for (const value of arr) {
if (!out.includes(value)) {
out.push(value);
}
}
return out;
}
This is logically correct — it does produce the right answer for [1, 2, 2, 3]. The problem is out.includes(value) walks the whole out array every iteration, so for an input of length n you do up to n work n times: O(n²). (Quick refresher: O(n) means runtime grows in proportion to input size — double the input, double the work. O(n²) means runtime grows with the square of the input — double the input, quadruple the work.) On a 10-item array it's fine. On a 10,000-item array it's a noticeable freeze. There's a second issue too: includes uses SameValueZero (good, so NaN would be handled correctly), but if a teammate later swaps it for indexOf to "save a character", they accidentally break NaN dedup — indexOf uses strict equality, and NaN !== NaN. A Set lookup is both faster and semantically locked to the right equality rule.
function uniqueArray(arr) {
// `seen` is our doorman's clipboard — every value we've already emitted.
// Set lookups (`has`, `add`) are average-case O(1) instead of O(n) for arr.includes.
const seen = new Set();
// We build a fresh output instead of mutating `arr`. The caller's array is theirs.
const out = [];
// Walk the input left to right so first-seen order is preserved naturally.
for (const value of arr) {
// Set uses SameValueZero, which means NaN equals NaN and -0 equals 0.
// That matches the standard "are these the same value?" check users expect.
if (!seen.has(value)) {
seen.add(value); // remember it before emitting, so the next duplicate gets blocked
out.push(value); // first sighting — keep it
}
// If `seen` already has `value`, we do nothing. The duplicate is dropped.
}
return out;
}
module.exports = { uniqueArray };
Two shifts from the naive version. First, the membership check moved from out.includes(...) (O(n) per call) to seen.has(...) (O(1) per call), so the whole function drops from O(n²) to O(n). Second, the equality semantics are now anchored by Set — SameValueZero, the same rule Array.prototype.includes uses — so NaN, -0, and 0 all behave the way most callers expect without any special-case code.
You'll sometimes see this written as:
const uniqueArray = (arr) => [...new Set(arr)];
It's the same algorithm. new Set(arr) walks the array once and deduplicates with SameValueZero; Set preserves insertion order, so spreading it back into an array gives you the original order with duplicates removed. Use whichever reads better — the explicit loop is friendlier when you want to log, branch, or extend the logic later.
Take uniqueArray([1, 2, 2, 3, 1, 4]) and step through it:
seen = Set{}, out = [].1 — seen.has(1) is false. Add 1 to seen, push 1 to out. Now seen = Set{1}, out = [1].2 — seen.has(2) is false. Add and push. seen = Set{1, 2}, out = [1, 2].2 again — seen.has(2) is true. Skip. seen and out unchanged.3 — seen.has(3) is false. Add and push. seen = Set{1, 2, 3}, out = [1, 2, 3].1 again — seen.has(1) is true. Skip.4 — seen.has(4) is false. Add and push. seen = Set{1, 2, 3, 4}, out = [1, 2, 3, 4].[1, 2, 3, 4]. The two duplicates dropped out exactly where they appeared in the input; the survivors are in first-seen order.NaN deserves its own momentNaN is the value JavaScript uses to mean "this number computation didn't produce a number" (e.g. 0 / 0). Quirk: NaN === NaN is false. So if you'd written the naive version with out.indexOf(value) === -1 instead of out.includes(value), NaN would never match itself and you'd end up with [NaN, NaN, NaN] in your output. Set (and includes) use SameValueZero, which treats NaN as equal to NaN — exactly what users expect.
Using indexOf instead of includes — arr.indexOf(NaN) is always -1, even when NaN is in the array, because indexOf uses strict equality (===). arr.includes(NaN) correctly returns true. If your input might contain NaN, never reach for indexOf to test membership.
Expecting objects to dedupe by shape — uniqueArray([{ a: 1 }, { a: 1 }]) keeps both objects. They look identical, but they're two different references in memory, and Set (like ===) compares references for objects. If you actually want value-based dedup, you have to serialize first (e.g. JSON.stringify) — and that has its own footguns (key order, undefined, circular references). For this question, reference equality is the right behavior.
Mutating the input — arr.splice or arr.filter with a side-effecting predicate would change the caller's array. The contract is "return a new array"; never touch arr.
Forgetting empty input — uniqueArray([]) should return [], not undefined. With the loop-and-set version, the loop body just doesn't run and you return the empty out — it falls out for free. With [...new Set(arr)], new Set([]) is empty and the spread produces []. Either way, don't add a special case.
Assuming Set randomizes order — Set preserves insertion order. That's why this whole algorithm works without an extra sort step. If you've used hash-based sets in other languages where iteration order is undefined, this is a JS-specific guarantee worth remembering.
Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
You're asked to deduplicate. Implement uniqueArray(arr): return a new array that contains each value from arr exactly once, in the order each value first appeared. Don't mutate the input; don't reorder things.
This is the same job as wrapping an array with new Set(...) and reading the result back out. You can use a Set, or roll your own bookkeeping — either way the contract is what matters.
function uniqueArray(arr) {
// returns a new array containing the distinct values of `arr`,
// in the order each value first appeared.
}
uniqueArray([1, 2, 2, 3, 1, 4]); // [1, 2, 3, 4]
uniqueArray(['a', 'b', 'a', 'c', 'b']); // ['a', 'b', 'c']
uniqueArray([]); // []
uniqueArray([NaN, NaN, 1, NaN]); // [NaN, 1] — NaN counts as equal to NaN
uniqueArray([{ a: 1 }, { a: 1 }]); // both kept — objects compare by reference
SameValueZero — the same rule Set and Array.prototype.includes use. NaN equals NaN, and -0 equals 0.[], not undefined.You'll walk the array once, remember every value you've already emitted in a Set, and skip anything you've seen before.
You have a list of values. Some are repeats. You want the list with each value kept only the first time it shows up — the second, third, and tenth appearances disappear. The output stays in the same order as the input, just without the echoes. That's what dedupe-preserving-order means, and it's the core of building things like a "recent searches" list or a tag picker that doesn't let the same tag appear twice.
Imagine reading the array left to right and writing values into an output list, but with a doorman at the entrance holding a clipboard. Each value walks up to the door; the doorman checks the clipboard for that value. If it's not there, the doorman writes it down and lets it through. If it's already on the clipboard, the value is turned away. The clipboard is your Set; the output list is what made it past the door.
A reasonable first try: walk the array and, for each value, check whether it's already in the output before pushing it.
function uniqueArrayBroken(arr) {
const out = [];
for (const value of arr) {
if (!out.includes(value)) {
out.push(value);
}
}
return out;
}
This is logically correct — it does produce the right answer for [1, 2, 2, 3]. The problem is out.includes(value) walks the whole out array every iteration, so for an input of length n you do up to n work n times: O(n²). (Quick refresher: O(n) means runtime grows in proportion to input size — double the input, double the work. O(n²) means runtime grows with the square of the input — double the input, quadruple the work.) On a 10-item array it's fine. On a 10,000-item array it's a noticeable freeze. There's a second issue too: includes uses SameValueZero (good, so NaN would be handled correctly), but if a teammate later swaps it for indexOf to "save a character", they accidentally break NaN dedup — indexOf uses strict equality, and NaN !== NaN. A Set lookup is both faster and semantically locked to the right equality rule.
function uniqueArray(arr) {
// `seen` is our doorman's clipboard — every value we've already emitted.
// Set lookups (`has`, `add`) are average-case O(1) instead of O(n) for arr.includes.
const seen = new Set();
// We build a fresh output instead of mutating `arr`. The caller's array is theirs.
const out = [];
// Walk the input left to right so first-seen order is preserved naturally.
for (const value of arr) {
// Set uses SameValueZero, which means NaN equals NaN and -0 equals 0.
// That matches the standard "are these the same value?" check users expect.
if (!seen.has(value)) {
seen.add(value); // remember it before emitting, so the next duplicate gets blocked
out.push(value); // first sighting — keep it
}
// If `seen` already has `value`, we do nothing. The duplicate is dropped.
}
return out;
}
module.exports = { uniqueArray };
Two shifts from the naive version. First, the membership check moved from out.includes(...) (O(n) per call) to seen.has(...) (O(1) per call), so the whole function drops from O(n²) to O(n). Second, the equality semantics are now anchored by Set — SameValueZero, the same rule Array.prototype.includes uses — so NaN, -0, and 0 all behave the way most callers expect without any special-case code.
You'll sometimes see this written as:
const uniqueArray = (arr) => [...new Set(arr)];
It's the same algorithm. new Set(arr) walks the array once and deduplicates with SameValueZero; Set preserves insertion order, so spreading it back into an array gives you the original order with duplicates removed. Use whichever reads better — the explicit loop is friendlier when you want to log, branch, or extend the logic later.
Take uniqueArray([1, 2, 2, 3, 1, 4]) and step through it:
seen = Set{}, out = [].1 — seen.has(1) is false. Add 1 to seen, push 1 to out. Now seen = Set{1}, out = [1].2 — seen.has(2) is false. Add and push. seen = Set{1, 2}, out = [1, 2].2 again — seen.has(2) is true. Skip. seen and out unchanged.3 — seen.has(3) is false. Add and push. seen = Set{1, 2, 3}, out = [1, 2, 3].1 again — seen.has(1) is true. Skip.4 — seen.has(4) is false. Add and push. seen = Set{1, 2, 3, 4}, out = [1, 2, 3, 4].[1, 2, 3, 4]. The two duplicates dropped out exactly where they appeared in the input; the survivors are in first-seen order.NaN deserves its own momentNaN is the value JavaScript uses to mean "this number computation didn't produce a number" (e.g. 0 / 0). Quirk: NaN === NaN is false. So if you'd written the naive version with out.indexOf(value) === -1 instead of out.includes(value), NaN would never match itself and you'd end up with [NaN, NaN, NaN] in your output. Set (and includes) use SameValueZero, which treats NaN as equal to NaN — exactly what users expect.
Using indexOf instead of includes — arr.indexOf(NaN) is always -1, even when NaN is in the array, because indexOf uses strict equality (===). arr.includes(NaN) correctly returns true. If your input might contain NaN, never reach for indexOf to test membership.
Expecting objects to dedupe by shape — uniqueArray([{ a: 1 }, { a: 1 }]) keeps both objects. They look identical, but they're two different references in memory, and Set (like ===) compares references for objects. If you actually want value-based dedup, you have to serialize first (e.g. JSON.stringify) — and that has its own footguns (key order, undefined, circular references). For this question, reference equality is the right behavior.
Mutating the input — arr.splice or arr.filter with a side-effecting predicate would change the caller's array. The contract is "return a new array"; never touch arr.
Forgetting empty input — uniqueArray([]) should return [], not undefined. With the loop-and-set version, the loop body just doesn't run and you return the empty out — it falls out for free. With [...new Set(arr)], new Set([]) is empty and the spread produces []. Either way, don't add a special case.
Assuming Set randomizes order — Set preserves insertion order. That's why this whole algorithm works without an extra sort step. If you've used hash-based sets in other languages where iteration order is undefined, this is a JS-specific guarantee worth remembering.
Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.