Array.prototype.reduce Polyfill
Implement Array.prototype.reduce from scratch, including the tricky no-initial-value path and the empty-array error case.
JavaScript · Arrays · Polyfills — Pratik Rai ·
Implement a function to flatten a nested array in JavaScript.
Flattening a nested array is the canonical recursion exercise, and it is asked constantly because it has an obvious elegant answer, a non-obvious failure mode, and a natural progression of follow-up questions. Array.prototype.flat solves it natively — but the interview is about what flat is doing underneath.
For the spec-accurate flat implementation including holes and depth handling, see the Array.prototype.flat polyfill. This page is about the underlying algorithms and their trade-offs.
JSfile.javascript1function flatten(input) { 2 const result = []; 3 for (const item of input) { 4 if (Array.isArray(item)) { 5 result.push(...flatten(item)); 6 } else { 7 result.push(item); 8 } 9 } 10 return result; 11}
Short and readable. Array.isArray is the correct test rather than instanceof Array, which returns false for arrays originating in another realm — an iframe, a worker, a Node vm context. Those are genuine arrays that instanceof misses.
A reduce variant reads more functionally and does the same work:
JSfile.javascript1const flatten = (input) => 2 input.reduce( 3 (acc, item) => acc.concat(Array.isArray(item) ? flatten(item) : item), 4 [] 5 );
Be aware this one is quadratic: concat allocates a new array on every element rather than appending in place. Elegant for a whiteboard, wrong for a large input.
Each nesting level consumes a stack frame. Engines cap stack depth at roughly ten to fifteen thousand frames, so a sufficiently deep structure throws:
JSfile.javascript1let deep = [1]; 2for (let i = 0; i < 50000; i++) deep = [deep]; 3flatten(deep); // RangeError: Maximum call stack size exceeded
Note this is about depth, not size. A million-element flat array is fine; a fifty-thousand-deep nest is not. Real data — JSON payloads, component trees, category hierarchies — is rarely more than a handful deep, so recursion is usually the right call. It becomes a liability when the shape is unbounded or comes from outside your system.
Replace the call stack with an explicit stack on the heap, which is bounded by memory rather than frame count:
JSfile.javascript1function flattenIterative(input) { 2 const stack = [...input]; 3 const result = []; 4 5 while (stack.length) { 6 const item = stack.pop(); 7 if (Array.isArray(item)) { 8 stack.push(...item); 9 } else { 10 result.push(item); 11 } 12 } 13 14 return result.reverse(); // popping from the end reverses order 15}
The reverse() at the end is the price of using pop, which is O(1). Using shift instead would preserve order naturally but is O(n) per operation, making the whole thing quadratic. Reversing once at the end is O(n) and clearly the better trade.
stack.push(...item) reintroduces the argument-count limit — for very wide inner arrays, loop instead of spreading.
Real code usually wants control over how far to go. Carry the remaining depth alongside each value:
JSfile.javascript1function flattenDepth(input, depth = 1) { 2 const result = []; 3 for (const item of input) { 4 if (depth > 0 && Array.isArray(item)) { 5 result.push(...flattenDepth(item, depth - 1)); 6 } else { 7 result.push(item); 8 } 9 } 10 return result; 11}
This mirrors native flat, whose default depth is 1 — a detail that surprises people expecting full flattening. Use flat(Infinity) for that.
arr.toString().split(',') works only for arrays of strings and numbers, and destroys types — every element comes back a string, null becomes an empty string, and nested undefined vanishes. It appears in blog posts as a clever trick; do not ship it.
JSON.parse('[' + JSON.stringify(arr).replace(/[\[\]]/g, '') + ']') is worse: it breaks on any string containing a bracket.
Both are the kind of answer that looks smart and fails a follow-up question about a string containing a comma.
All correct versions are O(n) in the total number of elements across every level — each is visited once. The differences are constant factors and memory:
concat-based reduce: O(n²) time — avoid on large inputs."Flatten an object instead." Turn {a: {b: {c: 1}}} into {'a.b.c': 1}. Same traversal, but you accumulate a key path and must decide how to treat arrays and null — typeof null === 'object' will catch you out.
"Handle circular references." Track visited arrays in a WeakSet and skip or throw on revisit. Native flat does not do this and will hang.
"Flatten lazily." A generator yields values without materialising the whole result, which matters when the caller only needs the first few:
JSfile.javascript1function* flattenLazy(input) { 2 for (const item of input) { 3 if (Array.isArray(item)) yield* flattenLazy(item); 4 else yield item; 5 } 6}
yield* delegating to a recursive generator is the neatest expression of the algorithm, though it still consumes stack depth per level.
Array.isArray, not instanceof, for cross-realm safety.concat in a reducer — it is quadratic.flat defaults to depth 1; pass Infinity for full flattening.Goal: Implement a function to flatten a nested array in JavaScript.
Continue learning with these related challenges
Implement Array.prototype.reduce from scratch, including the tricky no-initial-value path and the empty-array error case.
JavaScript · Arrays · Polyfills — Pratik Rai ·
Implement Array.prototype.flat from scratch with a configurable depth. Default depth is 1, not Infinity.
JavaScript · Arrays · Recursion — Pratik Rai ·
Compare two values structurally, because === only ever compares references.
JavaScript · ES6 — Pratik Rai ·
Implement Array.prototype.reduce from scratch, including the tricky no-initial-value path and the empty-array error case.
JavaScript · Arrays · Polyfills
Pratik Rai ·
Implement Array.prototype.flat from scratch with a configurable depth. Default depth is 1, not Infinity.
JavaScript · Arrays · Recursion
Pratik Rai ·
Compare two values structurally, because === only ever compares references.
JavaScript · ES6
Pratik Rai ·