Array.prototype.map Polyfill
Implement Array.prototype.map from scratch. Master callback signatures, thisArg binding, and sparse-array hole preservation.
JavaScript · Arrays · Polyfills — Pratik Rai ·
Implement Array.prototype.flat from scratch with a configurable depth. Default depth is 1, not Infinity.
Array.prototype.flat returns a new array with nested sub-arrays merged into it, up to a given depth. It arrived late — ES2019 — and the reason for the delay is a genuinely interesting piece of web history. Implementing it means deciding between recursion, iteration, and a stack, and knowing when each one falls over.
flat is a good exercise because the obvious recursive solution is elegant and has a real failure mode. It also has a default that surprises people, and a hole-removing behaviour that is not documented anywhere near prominently enough.
This is the most common flat mistake:
JSfile.javascript1[1, [2, [3, [4]]]].flat(); // [1, 2, [3, [4]]] — only one level 2[1, [2, [3, [4]]]].flat(2); // [1, 2, 3, [4]] 3[1, [2, [3, [4]]]].flat(Infinity); // [1, 2, 3, 4]
flat() with no argument flattens exactly one level, not all of them. If you want a fully flat array, you must pass Infinity explicitly.
A detail that is easy to miss and occasionally useful:
JSfile.javascript1[1, , 3].flat(); // [1, 3] — the hole is gone
flat skips empty slots entirely, even at depth 0. That makes arr.flat(0) a concise way to densify a sparse array, though arr.filter(() => true) says it more clearly.
The natural version reads almost like the specification:
JSfile.javascript1Array.prototype.myFlat = function (depth = 1) { 2 if (this === null || this === undefined) { 3 throw new TypeError('Array.prototype.myFlat called on null or undefined'); 4 } 5 6 const object = Object(this); 7 const length = object.length >>> 0; 8 const result = []; 9 10 for (let i = 0; i < length; i++) { 11 if (!(i in object)) continue; // skip holes 12 const value = object[i]; 13 if (depth > 0 && Array.isArray(value)) { 14 result.push(...value.myFlat(depth - 1)); 15 } else { 16 result.push(value); 17 } 18 } 19 20 return result; 21};
Array.isArray is the correct test rather than instanceof Array, which fails across realms — an array from an iframe or a Node vm context is a real array but not an instanceof match in the current realm.
Note this uses push(...), which reintroduces the argument-count limit from apply. On deeply nested data with very wide inner arrays, result.push(...huge) can throw a RangeError. Using a loop or result = result.concat(...) avoids it.
Every level of nesting adds a stack frame. Given a pathological input — a linked-list-shaped array nested tens of thousands deep — the recursive version blows the call stack:
JSfile.javascript1let deep = [1]; 2for (let i = 0; i < 100000; i++) deep = [deep]; 3deep.flat(Infinity); // native handles it; naive recursion throws
The iterative version uses an explicit stack on the heap instead, which is bounded by memory rather than by stack depth:
JSfile.javascript1function flatIterative(input, depth = 1) { 2 const stack = input.map((value) => [value, depth]); 3 const result = []; 4 5 while (stack.length) { 6 const [value, d] = stack.pop(); 7 if (d > 0 && Array.isArray(value)) { 8 for (let i = value.length - 1; i >= 0; i--) { 9 if (i in value) stack.push([value[i], d - 1]); 10 } 11 } else { 12 result.push(value); 13 } 14 } 15 16 return result; 17}
Pushing children in reverse and popping from the end preserves the original order while keeping the operation O(n).
For real-world data — JSON responses, component trees, nested menus — recursion is fine and reads far better. Reach for the iterative form when depth is genuinely unbounded or attacker-controlled.
flatMapflatMap is map followed by flat(1), in one pass:
JSfile.javascript1[1, 2, 3].flatMap((n) => [n, n * 2]); // [1, 2, 2, 4, 3, 6]
Its most useful property is that returning [] drops an element, which makes it a combined map-and-filter:
JSfile.javascript1users.flatMap((u) => (u.active ? [u.name] : []));
The depth is fixed at 1 and cannot be changed — flatMap(fn, 2) is not a thing.
"Flatten without recursion." The stack version above, or the loop-until-stable trick: while (arr.some(Array.isArray)) arr = [].concat(...arr); — concise, but quadratic in depth.
"What is the time complexity?" O(n) in the total number of elements across all levels, for both the recursive and iterative versions. The naive concat loop is O(n × depth) because it rebuilds the array each pass.
"Does it flatten array-likes?" No. Only genuine arrays are flattened; an object with a length is treated as a plain value.
Infinity.flat removes holes at every depth, including 0.Array.isArray, not instanceof, so it works across realms.flatMap is map plus flat(1), and returning [] drops the element.Goal: Implement myFlat with correct default depth and recursive depth tracking.
Continue learning with these related challenges
Implement Array.prototype.map from scratch. Master callback signatures, thisArg binding, and sparse-array hole preservation.
JavaScript · Arrays · Polyfills — Pratik Rai ·
Implement Array.prototype.filter from scratch. The key detail: push the original element, not the boolean result of the predicate.
JavaScript · Arrays · Polyfills — 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.map from scratch. Master callback signatures, thisArg binding, and sparse-array hole preservation.
JavaScript · Arrays · Polyfills
Pratik Rai ·
Implement Array.prototype.filter from scratch. The key detail: push the original element, not the boolean result of the predicate.
JavaScript · Arrays · Polyfills
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 ·