JavaScript
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Types
-
Section 3: Strings and Numbers in Depth
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Objects and Arrays
-
Section 7: Maps, Sets, and Symbols
-
Section 8: Asynchronous JavaScript
-
Section 9: Object-Oriented and Prototypes
-
Section 10: The DOM
-
Section 11: Browser APIs
-
Section 12: Modern JavaScript (ES2015-ES2025)
-
Section 13: Functional Programming Patterns
-
Section 14: Error Handling and Debugging
-
Section 15: Testing
-
Section 16: Accessibility for JavaScript Developers
-
Section 17: Internationalization and Localization
-
Section 18: Performance
-
Section 19: Node.js Fundamentals
-
Section 20: Regular Expressions
-
Section 21: Design Patterns in JavaScript
-
Section 22: Security Basics
-
Section 23: Data Structures and Algorithms in JavaScript
-
Section 24: Practical Projects
-
Section 25: More Advanced Async Patterns
-
Section 26: More Object and Class Practice
-
Section 27: Working with Dates and Internationalization
-
Section 28: Web Components
-
Section 29: More DOM and Browser Practice
-
Section 30: Build Tooling for Vanilla JavaScript
-
Section 31: More Practice Projects
-
Section 32: Interview and Algorithm Practice
-
Section 33: Error Objects (MDN Reference)
-
Section 34: TypedArrays and Binary Data
-
Section 35: Reflection and Metaprogramming (MDN Reference)
-
Section 36: More Global Functions (MDN Reference)
35: Recursion in JavaScript
I'll be honest with you: recursion is one of those concepts that feels like a "eureka" moment when it first clicks, but it's also one of the easiest ways to accidentally crash your application. At its simplest, recursion is just a function calling itself. It's a beautiful way to handle data structures where you don't know how deep the nesting goes—like a file system or a nested comment thread on a blog.
The Elegance of the Naive Approach
Let's look at a classic problem: flattening a deeply nested array. You might have an array like [1, [2, [3, [4]]]] and you want it to be [1, 2, 3, 4]. The naive recursive approach is incredibly clean. You just check if an element is an array; if it is, you call the function again on that element. If it's not, you keep it.
function flattenNaive(arr) {
let result = [];
arr.forEach(item => {
if (Array.isArray(item)) {
result = result.concat(flattenNaive(item));
} else {
result.push(item);
}
});
return result;
}
When you look at this code, it feels "correct." It's declarative and mirrors the structure of the data. For most small-to-medium datasets, this works perfectly. You write it in five minutes, it passes the tests, and you move on. But this is where the "junior" mistake happens: assuming that because it works for 10 levels of nesting, it will work for 10,000.
When the Call Stack Hits a Wall
Here is the problem I've run into in production more than once: the Call Stack. Every time JavaScript calls a function, it adds a "frame" to the stack to remember where it was and what the local variables were. In the naive version above, the function can't actually finish and return its result until the inner recursive call finishes.
If you pass an array nested 20,000 levels deep into flattenNaive, you aren't just doing a lot of work; you're asking JavaScript to remember 20,000 different unfinished function calls. Eventually, the engine gives up and throws a RangeError: Maximum call stack size exceeded. You've hit the ceiling. It's a hard crash, and there's no try...catch that's going to make that recursion suddenly "fit" in memory.
Trading Elegance for Stability
If you're working with potentially massive datasets, I usually advise my colleagues to ditch the recursive call in favor of a manual stack. Instead of letting the JavaScript engine manage the "to-do list" via the call stack, you manage it yourself using a standard array. This moves the memory usage from the call stack (which is very small) to the heap (which is much larger).
function flattenIterative(input) {
const stack = [...input];
const result = [];
while (stack.length > 0) {
const next = stack.pop();
if (Array.isArray(next)) {
// Push contents back onto the stack to be processed
stack.push(...next);
} else {
result.push(next);
}
}
// Since we popped from the end, we need to reverse the result
return result.reverse();
}
Now, look at the trade-off. We lost that "mathematical" beauty of the first example. The code is a bit more verbose and requires a .reverse() at the end because we're processing the array from the back. But here's the win: this version can handle an array nested a million levels deep without breaking a sweat. It doesn't add new frames to the call stack; it just modifies a single array in memory.
My rule of thumb? Use recursion when the depth is guaranteed to be shallow (like a UI component tree that's rarely more than 10 levels deep). But the moment you're dealing with user-generated data or massive datasets, reach for the iterative approach. Your on-call rotation will thank you.
📋 Practical Task
Exercise: Deeply Nested Category Tree Flattener
You are building an e-commerce site that has categories and sub-categories nested to an unknown depth. You've been given a "Category Tree" object, but the search index requires a flat list of all category names.
The Data:
const categoryTree = [
{
name: "Electronics",
children: [
{
name: "Laptops",
children: [
{ name: "Gaming Laptops", children: [] },
{ name: "Ultrabooks", children: [] }
]
},
{ name: "Cameras", children: [] }
]
},
{
name: "Clothing",
children: [
{ name: "Mens", children: [] },
{ name: "Womens", children: [] }
]
}
];
Your Task: Write a function called getAllCategoryNames that takes this tree and returns a flat array of strings: ["Electronics", "Laptops", "Gaming Laptops", "Ultrabooks", "Cameras", "Clothing", "Mens", "Womens"].
Constraint: To ensure the application doesn't crash on extremely large catalogs, implement this using a manual stack (iterative approach) rather than recursion.
There are no comments for now.