Flatten Object Keys
intermediateTurn nested objects into dot-path keys ({ 'a.b.c': 1 }) and back — the transform behind form libraries and analytics events.
Implement deep flatten in JavaScript three ways — recursion, an explicit stack, and a lazy generator — with the O(n²) concat trap and Array.prototype.flat comparison.
Flatten an arbitrarily nested array —
[1, [2, [3, [4]]]]→[1, 2, 3, 4]— without usingArray.prototype.flat.
This is the friendliest recursion drill in the catalog, which is exactly why interviewers use it: the base solution takes three minutes, leaving time to probe what they actually care about — complexity of your version, converting recursion to iteration, and whether you know the platform already ships the answer (arr.flat(Infinity), ES2019) and what its semantics are.
function deepFlatten(arr) {
const result = [];
for (const item of arr) {
if (Array.isArray(item)) {
result.push(...deepFlatten(item)); // recurse, then splice results in
} else {
result.push(item);
}
}
return result;
}
deepFlatten([1, [2, [3, [4, [5]]]], 6]); // [1, 2, 3, 4, 5, 6]One deliberate choice to narrate: push into one result array, don't concat in a reduce. The elegant-looking reduce((acc, v) => acc.concat(...), []) allocates a new array on every element — O(n²) across the run. It's the identical quadratic-spread trap as in series execution, and calling it out unprompted converts a warm-up into a signal.
Also: Array.isArray, not instanceof Array — the check survives cross-realm arrays (the iframe story).
A 10,000-deep nesting blows the call stack; the fix is owning the stack yourself:
function deepFlattenIterative(arr) {
const stack = [...arr];
const result = [];
while (stack.length > 0) {
const next = stack.pop();
if (Array.isArray(next)) {
stack.push(...next); // unpack one level; children get re-examined
} else {
result.push(next);
}
}
return result.reverse(); // LIFO processed us back-to-front
}The reverse() at the end is the tell that you actually traced the algorithm: pop takes from the tail, so elements land in reverse order. (Alternatives: shift from the front — O(n) per removal, worse — or push results and reverse once, as here: O(n) total.) This recursion→stack conversion is the same move asked in JSON.parse and deep clone follow-ups — one skill, three costumes.
function* flattenGen(arr) {
for (const item of arr) {
if (Array.isArray(item)) {
yield* flattenGen(item); // delegate — recursion in generator form
} else {
yield item;
}
}
}
[...flattenGen([1, [2, [3]]])]; // [1, 2, 3] — materialize when needed
for (const x of flattenGen(huge)) { // …or stream without building the array
if (found(x)) break; // early exit does NO work on the rest
}The generator changes the cost model: nothing materializes until consumed, and early exit skips the untraversed remainder. That's the same lazy-pipeline idea as iterator helpers, and offering it with the sentence "if the consumer might not need everything, flattening lazily is free" is the strongest ending this question has.
function flattenDepth(arr, depth = 1) {
if (depth < 1) return arr.slice();
const result = [];
for (const item of arr) {
if (Array.isArray(item) && depth >= 1) {
result.push(...flattenDepth(item, depth - 1));
} else {
result.push(item);
}
}
return result;
}
flattenDepth([1, [2, [3, [4]]]], 1); // [1, 2, [3, [4]]] — flat()'s default!
flattenDepth([1, [2, [3, [4]]]], 2); // [1, 2, 3, [4]]Matching the built-in's contract matters: arr.flat() defaults to depth 1, not Infinity — a spec detail candidates trip on constantly. deepFlatten ≡ flattenDepth(arr, Infinity) ≡ arr.flat(Infinity).
[1, [], [2, []]] → [1, 2]; falls out naturally (nothing to iterate), worth stating.flat removes holes ([1, , 2].flat() → [1, 2]); for...of visits holes as undefined, so your version keeps them as undefined. A named divergence from the built-in — knowing it exists beats accidentally matching it.Array.isArray gate); flattening strings char-by-char is the classic overreach bug.flat also has no cycle protection (it's specced on finite structures). If asked, the WeakSet-of-ancestors pattern from deep clone applies.yield* — so really solution 2) is the survivable one. Precision on which alternatives fix the stack is a nice catch: yield* delegation nests stack frames too.concat-in-reduce.toString().split(",") "solutions" — destroys types ("1" not 1), breaks on elements containing commas; it's a party trick, not an answer.reverse() in the pop-based iterative version (or "fixing" order with shift and going quadratic).flat() defaults to full depth.instanceof Array without the cross-realm caveat.a.b.c naming policy); recognizing it's not this algorithm — the recursion is similar but the output contract is a design discussion — is the point.flat exist when flatMap exists?" — flatMap is map+flatten(1) fused in one pass — and arr.flatMap(x => x) is a one-level flatten; the fusion idea connects to pipe and lazy pipelines.for await + yield*) — the natural bridge to async iteration.Turn nested objects into dot-path keys ({ 'a.b.c': 1 }) and back — the transform behind form libraries and analytics events.
Structural equality for nested data: type checks, key comparison, and recursion done right.
Use a Proxy to support arr[-1] like Python — a practical introduction to Proxy traps.
Recursively clone nested objects and arrays, handling cycles with a WeakMap.