Lesson 1.5.3 — Recursion
0. Metadata (Thông tin bài học)
| Thuộc tính | Giá trị |
|---|---|
| Stage | 1 — JavaScript Execution Model |
| Module | 1.5 — Call Stack & Execution Tracing |
| Lesson | 1.5.3 — Recursion |
| Competency | C02 — JavaScript Runtime (C02.6 Call Stack) |
| Depth Target | L3–L4 (Use / Debug) |
| Prerequisites | Function call & return (Stage 0), Stack Frames (Lesson 1.5.1), Nested Calls (Lesson 1.5.2) |
| Cognitive Load | Medium–High |
1. Why This Exists (Vì sao cần học)
Bạn đã trace được a() → b() → c(). Nhưng recursion tạo ra một tình huống khác: cùng một function có thể xuất hiện nhiều lần trên Call Stack, vì mỗi lần gọi là một invocation độc lập.
Xem đoạn code sau:
function countdown(n) {
if (n === 0) return;
console.log(n);
countdown(n - 1);
}
countdown(3);Source code chỉ có một function countdown, nhưng trong lúc chạy có thể đồng thời tồn tại countdown(3), countdown(2), countdown(1) và countdown(0).
Nếu không có mental model về Call Stack, recursion thường bị học thành một mẹo: "function tự gọi chính nó". Cách học đó không đủ để trả lời các câu quan trọng hơn: mỗi lần gọi có n riêng hay dùng chung, vì sao cần base case, khi nào stack bắt đầu giảm, và vì sao một recursive bug có thể dẫn tới RangeError: Maximum call stack size exceeded.
Learning Goal (Mục tiêu học)
Bài này không dạy recursion như một chủ đề thuật toán. Mục tiêu là dùng recursion để làm rõ nhiều invocation của cùng một function được tổ chức trên Call Stack như thế nào, và dùng mental model đó để debug lỗi dừng đệ quy.
2. Prerequisites (Yêu cầu đầu vào)
Trước khi học bài này, bạn cần:
- [ ] Biết mỗi function call tạo ra một invocation mới.
- [ ] Vẽ được Call Stack theo mô hình
push → execute → pop. - [ ] Trace được caller → callee → resume trong nested calls.
- [ ] Biết
returnkết thúc invocation hiện tại và trả control về caller. - [ ] Phân biệt được local binding của các invocation khác nhau.
Nếu bạn vẫn nhìn countdown(3) và countdown(2) như "cùng một lần chạy của function", quay lại Lesson 1.5.1 — Stack Frames và Lesson 1.5.2 — Nested Calls trước.
3. Learning Objectives (Mục tiêu học tập)
Sau bài này, bạn có thể:
- Giải thích recursion bằng invocation và Call Stack thay vì chỉ nói "function gọi chính nó".
- Trace stack growth của một recursive function đơn giản qua 3–5 invocation.
- Xác định base case và recursive step trong một function đệ quy.
- Dự đoán thời điểm stack ngừng tăng và bắt đầu pop.
- Phân biệt local bindings của nhiều invocation cùng một function.
- Debug hai lỗi phổ biến: thiếu base case và recursive step không tiến gần base case.
- Giải thích nguyên nhân conceptual của stack overflow mà không phụ thuộc vào một con số giới hạn cụ thể của engine.
4. Mental Model (Mô hình tư duy)
Recursion không tạo ra một "function đặc biệt". Nó vẫn tuân theo rule đã học: mỗi function call tạo ra một invocation mới.
Với countdown(3), hãy hình dung:
countdown(3)
↓ calls
countdown(2)
↓ calls
countdown(1)
↓ calls
countdown(0)
↓ base case returns
countdown(1) resumes and ends
↓
countdown(2) resumes and ends
↓
countdown(3) resumes and endsCall Stack có hai pha dễ quan sát:
GROW
Global
Global → countdown(3)
Global → countdown(3) → countdown(2)
Global → countdown(3) → countdown(2) → countdown(1)
Global → countdown(3) → countdown(2) → countdown(1) → countdown(0)
SHRINK
Global → countdown(3) → countdown(2) → countdown(1)
Global → countdown(3) → countdown(2)
Global → countdown(3)
GlobalMental Model (Mô hình tư duy)
Một recursive function cần hai điều để stack có thể quay trở lại trạng thái cũ: base case phải có khả năng kết thúc một invocation, và recursive step phải làm input tiến dần tới base case. Nếu cứ tạo invocation mới mà không đạt điểm dừng, stack tiếp tục tăng cho tới khi runtime không thể cấp thêm stack space cho chuỗi call đó.
Ba câu hỏi phải luôn hỏi khi trace recursion
Tại mỗi recursive call, hỏi:
- Invocation hiện tại đang giữ local input nào?
- Recursive call tiếp theo nhận input nào?
- Input mới có thực sự tiến gần base case không?
Nếu không trả lời được câu 3, bạn chưa chứng minh recursion sẽ dừng.
5. Core Concepts (Các khái niệm cốt lõi)
Essential (Bắt buộc)
| Khái niệm | Ý nghĩa trong lesson |
|---|---|
| Recursion | Một execution path trong đó function trực tiếp hoặc gián tiếp dẫn tới một invocation mới của chính function đó. |
| Recursive invocation | Một lần gọi cụ thể như countdown(3) hoặc countdown(2); mỗi invocation có execution state riêng. |
| Base case | Nhánh kết thúc recursion mà không tạo thêm recursive call. |
| Recursive step | Nhánh tạo recursive call tiếp theo với state/input mới. |
| Stack growth | Số invocation đang active tăng vì caller cũ chưa kết thúc trước khi callee mới được gọi. |
| Stack shrink | Các invocation hoàn tất và frame lần lượt pop theo thứ tự LIFO. |
| Stack overflow | Tình trạng chuỗi call dùng vượt quá khả năng stack hiện tại của runtime, thường quan sát được bằng lỗi như RangeError: Maximum call stack size exceeded. |
Supporting (Hỗ trợ)
- Cùng một function có thể có nhiều frame active cùng lúc.
- Mỗi invocation có parameter/local bindings riêng, ví dụ
n = 3,n = 2,n = 1. - Base case không "xóa toàn bộ stack"; nó chỉ kết thúc invocation hiện tại, sau đó control quay lại caller ngay bên dưới.
- Unwind bình thường của recursion là chuỗi return/pop theo LIFO.
- Một base case tồn tại trong source chưa đủ; execution path thực tế phải có khả năng đi tới nó.
Awareness (Biết tồn tại)
- Recursion có thể là trực tiếp như
f() → f()hoặc gián tiếp nhưa() → b() → a(). - Giới hạn stack không phải hằng số JavaScript universal; nó phụ thuộc runtime, engine, environment và execution context.
- Một số bài toán có thể viết bằng recursion hoặc iteration, nhưng quyết định thuật toán chi tiết không phải mục tiêu của lesson này.
Out of Scope (Không học trong bài này)
- Recursion algorithms như tree traversal nâng cao, backtracking, divide-and-conquer.
- Dynamic programming và memoization cho recursive algorithms.
- Tail-call optimization / proper tail calls ở mức implementation.
- Engine stack layout, stack pointer, native stack internals.
- Exception-driven stack unwinding — Lesson 1.5.4.
- Async recursion, Promise, Event Loop — Stage 3.
6. Worked Example (Ví dụ phân tích từng bước)
Ta dùng một recursive function có cả code trước và sau recursive call để thấy rõ hai pha grow/shrink:
function countdown(n) {
if (n === 0) {
console.log("go");
return;
}
console.log("down", n);
countdown(n - 1);
console.log("up", n);
}
countdown(3);Step 1 — Phân loại trước khi trace
Base case là n === 0. Khi điều kiện này đúng, invocation hiện tại in "go" rồi return, không tạo recursive call mới.
Recursive step là countdown(n - 1). Với input dương, n - 1 làm giá trị tiến dần về 0.
Trước khi chạy code, ta đã có bằng chứng conceptual rằng chuỗi 3 → 2 → 1 → 0 sẽ chạm base case.
Step 2 — countdown(3) bắt đầu
n = 3, nên base case sai. Function in down 3, rồi gọi countdown(2).
Stack lúc đó:
Global
└── countdown(3)
└── countdown(2)countdown(3) chưa chạy được console.log("up", 3) vì đang chờ recursive call hoàn tất.
Step 3 — Stack tiếp tục grow
countdown(2) in down 2 rồi gọi countdown(1). Sau đó countdown(1) in down 1 rồi gọi countdown(0).
Stack sâu nhất:
Global
└── countdown(3) n = 3
└── countdown(2) n = 2
└── countdown(1) n = 1
└── countdown(0) n = 0Điểm cần chú ý: bốn invocation đều là cùng function countdown, nhưng chúng không dùng chung một parameter n duy nhất. Mỗi invocation có binding n của riêng nó.
Step 4 — Base case dừng việc tạo frame mới
Trong countdown(0), điều kiện n === 0 đúng. Function in go rồi return.
Base case chỉ làm một việc trực tiếp: không gọi countdown(...) thêm lần nữa.
Frame countdown(0) pop. Control quay về countdown(1) ngay sau call site countdown(n - 1);.
Step 5 — Stack bắt đầu shrink
countdown(1) resume và chạy console.log("up", 1), sau đó kết thúc.
Tiếp theo countdown(2) resume và in up 2. Cuối cùng countdown(3) resume và in up 3.
Return/resume order là:
countdown(0)
↓ returns to
countdown(1)
↓ returns to
countdown(2)
↓ returns to
countdown(3)
↓ returns to
GlobalStep 6 — Dự đoán output hoàn chỉnh
Output là:
down 3
down 2
down 1
go
up 1
up 2
up 3down xuất hiện theo chiều stack grow. up xuất hiện theo chiều stack shrink.
Quan sát quan trọng
Recursion không "quay ngược code" sau base case. Mỗi caller chỉ resume tại statement ngay sau recursive call của chính invocation đó. Vì các caller được resume theo LIFO nên output tạo cảm giác đi ngược từ 1 → 2 → 3.
7. Prediction Exercise (Bài tập dự đoán)
Bài 1 — Stack sâu nhất
Đừng chạy code. Hãy xác định stack sâu nhất và output.
function walk(n) {
console.log(n);
if (n === 1) return;
walk(n - 1);
}
walk(4);Đáp án & Giải thích
Output là 4, 3, 2, 1 trên bốn dòng.
Stack sâu nhất là:
Global
└── walk(4)
└── walk(3)
└── walk(2)
└── walk(1)walk(1) chạm base case và không tạo call mới. Sau đó các invocation pop theo thứ tự walk(1) → walk(2) → walk(3) → walk(4).
Bài 2 — Có base case nhưng vẫn không dừng
Đừng chạy code. Function sau có base case. Nó có kết thúc khi gọi countdown(3) không?
function countdown(n) {
if (n === 0) return;
countdown(n + 1);
}
countdown(3);Đáp án & Giải thích
Không. Execution path là 3 → 4 → 5 → 6 → ..., nên input đi xa dần base case 0.
Có base case trong source code không đồng nghĩa recursion chắc chắn kết thúc. Recursive step phải làm state tiến tới điều kiện dừng.
Trong execution này, stack tiếp tục grow cho tới khi runtime throw stack-overflow-related error.
Bài 3 — Local bindings có dùng chung không?
Đừng chạy code. Khi show(1) đang chạy, giá trị n của invocation show(3) là gì?
function show(n) {
if (n === 0) return;
show(n - 1);
console.log(n);
}
show(3);Đáp án & Giải thích
Binding n của show(3) vẫn là 3. Invocation show(2) có n = 2, còn show(1) có n = 1.
Các invocation cùng chạy từ một function definition, nhưng mỗi invocation có execution state riêng. Vì vậy sau khi show(1) kết thúc, caller show(2) vẫn resume với n = 2; sau đó show(3) resume với n = 3.
Output là 1, 2, 3 trên ba dòng.
Bài 4 — Transfer Check
Function sau xử lý một cấu trúc lồng nhau rất nhỏ:
function printCategory(category) {
console.log(category.name);
if (!category.child) return;
printCategory(category.child);
}
const category = {
name: "Frontend",
child: {
name: "JavaScript",
child: {
name: "Execution Model",
child: null,
},
},
};
printCategory(category);Đừng chạy code. Hãy chỉ ra base case theo data shape, không theo number.
Đáp án & Giải thích
Base case là !category.child. Khi node hiện tại không còn child, function return và không tạo recursive invocation mới.
Recursive step là printCategory(category.child). Mỗi call chuyển sang một object con nằm sâu hơn trong cấu trúc.
Bài này kiểm tra transfer: base case không bắt buộc phải là n === 0; nó là điều kiện khiến execution path ngừng tạo recursive call mới.
8. Implementation Lab (Bài lab thực hành)
Lab 1 — Complete the Base Case (Guided)
Hoàn thiện function để in 3, 2, 1 rồi dừng:
function countdown(n) {
// TODO: base case
console.log(n);
countdown(n - 1);
}
countdown(3);Yêu cầu:
- Không dùng loop.
- Không thay đổi recursive step
countdown(n - 1). - Base case phải nằm trước
console.log(n)để không in0.
Đáp án Lab 1
function countdown(n) {
if (n === 0) return;
console.log(n);
countdown(n - 1);
}
countdown(3);Với input 3, state đi theo 3 → 2 → 1 → 0. Khi n = 0, invocation return trước khi log và trước khi tạo call mới.
Lab 2 — Trace Before Fix (Partial Scaffold)
Code sau đáng lẽ in 3, 2, 1 rồi dừng nhưng lại overflow:
function countdown(n) {
if (n === 0) return;
console.log(n);
countdown(n + 1);
}
countdown(3);Trước khi sửa, viết ra năm input đầu tiên của chuỗi recursive invocation. Sau đó chỉ sửa một expression.
Đáp án Lab 2
Năm input đầu tiên là 3 → 4 → 5 → 6 → 7. Chuỗi đang đi xa base case 0.
Sửa recursive step thành countdown(n - 1);.
function countdown(n) {
if (n === 0) return;
console.log(n);
countdown(n - 1);
}
countdown(3);Bug không phải "recursion quá sâu" theo nghĩa input ban đầu lớn. Root cause là progress direction sai.
Lab 3 — Independent: Recursive Sum nhỏ
Viết sumTo(n) với contract:
sumTo(1)trả1.sumTo(2)trả3.sumTo(4)trả10.- Chỉ cần hỗ trợ integer
n >= 1trong lab này. - Không dùng loop.
Trước khi code, viết recurrence bằng ngôn ngữ tự nhiên: sumTo(n) cần kết quả của invocation nhỏ hơn nào?
Đáp án Lab 3
Một implementation phù hợp:
function sumTo(n) {
if (n === 1) return 1;
return n + sumTo(n - 1);
}
console.log(sumTo(4));Trace value flow chính là sumTo(4) = 4 + sumTo(3), sumTo(3) = 3 + sumTo(2), sumTo(2) = 2 + sumTo(1), và base case sumTo(1) = 1.
Khi stack shrink, value lần lượt trở thành 1 → 3 → 6 → 10.
Lab 4 — Constraint-based Trace
Cho code:
function echoDepth(n) {
if (n === 0) return "done";
const result = echoDepth(n - 1);
return `${n}:${result}`;
}
const output = echoDepth(3);Không chạy code. Hãy tạo bảng trace gồm invocation, n, đang chờ gì, return value.
Đáp án Lab 4
| Invocation | n | Đang chờ | Return value |
|---|---|---|---|
echoDepth(3) | 3 | echoDepth(2) | "3:2:1:done" |
echoDepth(2) | 2 | echoDepth(1) | "2:1:done" |
echoDepth(1) | 1 | echoDepth(0) | "1:done" |
echoDepth(0) | 0 | Không | "done" |
Bài này nối kiến thức của 1.5.2 với recursion: mỗi caller giữ state riêng trong khi chờ callee, rồi dùng return value khi resume.
9. Edge Cases (Các trường hợp ngoại lệ)
Edge Case 1 — Base case đặt sau recursive call
function countdown(n) {
countdown(n - 1);
if (n === 0) return;
}
countdown(3);Base case không được thực thi kịp
Khi countdown(0) bắt đầu, function lập tức gọi countdown(-1) trước khi kiểm tra n === 0. Vì vậy execution không dừng ở 0. Với pattern này, stack tiếp tục grow.
Edge Case 2 — Input ban đầu đã vượt khỏi domain giả định
function countdown(n) {
if (n === 0) return;
countdown(n - 1);
}
countdown(-1);Với contract ngầm "chỉ nhận integer n >= 0", input -1 vi phạm domain. Chuỗi trở thành -1 → -2 → -3 → ..., không đi tới 0.
Contract cũng là một phần của termination reasoning
Một recursive function có thể đúng trong domain đã thiết kế nhưng không dừng với input ngoài domain. Khi debug, đừng chỉ kiểm tra base case; kiểm tra cả input contract.
Edge Case 3 — Mutual recursion
function isEven(n) {
if (n === 0) return true;
return isOdd(n - 1);
}
function isOdd(n) {
if (n === 0) return false;
return isEven(n - 1);
}
console.log(isEven(4));Đây là recursion gián tiếp: isEven → isOdd → isEven. Call Stack vẫn tuân theo cùng rule push/pop; chỉ khác là function name luân phiên.
Awareness
Bạn chỉ cần nhận biết mutual recursion ở Stage 1. Không cần biến lesson thành bài chứng minh thuật toán hoặc thiết kế recursive state machine.
10. Debug Lab (Bài lab gỡ lỗi)
Symptom (Triệu chứng): Gọi repeat(3) khiến runtime throw lỗi stack overflow thay vì dừng sau ba bước.
Reproduction (Tái hiện lỗi):
function repeat(n) {
if (n === 0) return;
console.log(n);
repeat(n);
}
repeat(3);Evidence (Bằng chứng): Log lặp lại 3; input của invocation mới không thay đổi.
Hypothesis (Giả thuyết): Base case tồn tại nhưng recursive step không làm state tiến tới 0.
Verification (Xác minh): Năm invocation đầu tiên đều là repeat(3) → repeat(3) → repeat(3) → repeat(3) → repeat(3).
Root Cause (Nguyên nhân gốc rễ): Recursive step dùng lại n thay vì tạo state mới gần base case hơn.
Fix (Sửa lỗi): Đổi recursive call thành repeat(n - 1);.
function repeat(n) {
if (n === 0) return;
console.log(n);
repeat(n - 1);
}
repeat(3);Prevention (Phòng ngừa):
- Viết rõ base case trước khi viết recursive step.
- Với mỗi recursive call, kiểm tra state mới khác state cũ như thế nào.
- Trace 3–5 invocation đầu bằng tay trước khi chạy input lớn hơn.
- Nếu stack overflow, tìm cycle của input/state thay vì chỉ nhìn số lượng dòng code.
11. Design / Decision Exercise (Bài tập lựa chọn cách triển khai)
Bạn cần xử lý một cấu trúc category chỉ có một child nối tiếp như sau:
const category = {
name: "A",
child: {
name: "B",
child: {
name: "C",
child: null,
},
},
};Có hai implementation hợp lệ:
function printNames(node) {
console.log(node.name);
if (!node.child) return;
printNames(node.child);
}function printNames(node) {
let current = node;
while (current) {
console.log(current.name);
current = current.child;
}
}Câu hỏi: Có phải recursion luôn là lựa chọn tốt hơn vì data đang nested không?
Đáp án tham khảo
Không. Cả hai implementation đều có thể đúng với contract hiện tại.
Ở depth của lesson này, decision lens chỉ cần gồm:
- Recursion có thể map tự nhiên với cấu trúc tự tham chiếu.
- Mỗi recursive invocation làm tăng active call depth cho tới base case.
- Iteration có thể tránh tăng call depth theo số node trong chuỗi.
- Nếu depth của data có thể rất lớn hoặc không được kiểm soát, stack behavior trở thành một constraint cần cân nhắc.
Không cần kết luận "recursion xấu" hay "iteration tốt hơn" một cách tuyệt đối.
12. Production Scenario (Tình huống thực tế)
Một ứng dụng nhận category tree từ backend. Team viết utility để tìm tên category sâu nhất trong một nhánh:
function getDeepestName(category) {
if (!category.child) return category.name;
return getDeepestName(category.child);
}Code hoạt động với data test chỉ sâu 3–5 tầng. Sau đó production nhận dữ liệu lỗi do upstream tạo ra một chuỗi child rất sâu.
Câu hỏi:
- Vì sao logic có thể đúng về mặt functional nhưng vẫn gặp vấn đề runtime?
- Tín hiệu nào cho thấy Call Stack liên quan?
- Root cause nên được phân loại là "recursion luôn nguy hiểm" hay "depth không được kiểm soát"?
- Bạn cần xác minh constraint nào trước khi quyết định giữ recursion hay đổi implementation?
Đáp án tham khảo
- Mỗi node chưa phải base case tạo thêm một recursive invocation. Với depth đủ lớn, số active frame có thể vượt stack capacity của runtime trước khi chạm node cuối.
- Stack-overflow-related error và stack trace lặp lại cùng function là tín hiệu mạnh.
- Root cause cụ thể là input depth không được kiểm soát hoặc không khớp assumption của implementation, không phải kết luận chung rằng mọi recursion đều sai.
- Cần biết maximum expected depth, mức độ tin cậy của dữ liệu đầu vào, cách validate dữ liệu và yêu cầu reliability. Nếu depth có thể rất lớn hoặc hostile/untrusted, implementation dựa trên call depth cần được đánh giá lại.
Production Lens (Góc nhìn production)
Recursive code thường trông ngắn và đúng với data nested, nhưng production decision phải tính cả maximum depth, input trust và failure mode. Mental model Call Stack giúp bạn nhìn thấy constraint này trước khi bug xảy ra.
13. AI-Assisted Exercise (Bài tập với AI)
Level 2 — Challenge (Thách thức)
- Tự giải thích trước: vì sao function có
if (n === 0) return;vẫn có thể overflow? - Hỏi AI: "Can a recursive JavaScript function have a base case and still overflow the call stack? Explain using stack growth and progress toward the base case."
- Kiểm tra xem AI có nói sai rằng "chỉ cần có base case thì recursion chắc chắn an toàn" hay đưa ra một con số cố định cho maximum call stack depth không.
- Tạo phản ví dụ bằng
countdown(n + 1)hoặcrepeat(n)và yêu cầu AI trace năm invocation đầu.
Đáp án tham khảo
Một câu trả lời tốt phải tách ba điều:
- Base case tồn tại: source code có nhánh không recurse.
- Reachability: execution path thực tế có đi tới base case hay không.
- Resource bound: ngay cả khi về lý thuyết sẽ dừng, depth quá lớn vẫn có thể vượt stack capacity trước khi tới điểm dừng.
Nếu AI đưa ra một con số universal như "JavaScript stack luôn tối đa 10.000 calls", đó là dấu hiệu cần challenge. Giới hạn thực tế phụ thuộc engine/runtime/environment và không phải contract cố định của language.
14. Teach Back (Dạy lại)
Trong 2 phút, hãy giải thích cho một junior đoạn code sau mà không dùng câu "recursion là function gọi chính nó" làm lời giải chính:
function count(n) {
if (n === 0) return;
count(n - 1);
}
count(3);Bắt buộc giải thích đủ năm ý: invocation, local n, stack growth, base case, stack shrink.
Mô phỏng
count(3) tạo một invocation có n = 3. Vì chưa chạm base case, nó gọi count(2), tạo invocation mới với binding n = 2 trong khi count(3) vẫn đang active. Quá trình tiếp tục với count(1) rồi count(0), nên Call Stack grow. count(0) chạm base case và return mà không tạo call mới. Từ đây frame pop theo LIFO: control quay về count(1), rồi count(2), rồi count(3), nên stack shrink về Global. Mỗi invocation dùng cùng function definition nhưng có execution state riêng.
Gợi ý đánh giá bản thân
- Bạn có giải thích được vì sao
count(3)vẫn giữn = 3khicount(1)đang chạy không? - Bạn có nói rõ base case ngăn tạo invocation mới không?
- Bạn có phân biệt "có base case" với "execution chắc chắn đi tới base case" không?
15. Assessment (Đánh giá)
| Objective (Mục tiêu) | Hình thức đánh giá | Depth (Độ sâu) |
|---|---|---|
| Giải thích recursion bằng invocation + Call Stack | Explain (Giải thích) | L2–L3 |
| Trace stack growth/shrink 3–5 tầng | Trace (Theo vết thực thi) | L3 |
| Xác định base case và recursive step | Classification (Phân loại) | L3 |
| Dự đoán local bindings và output | Prediction (Dự đoán) | L3 |
| Implement recursive function đơn giản có termination rõ ràng | Use / Implement small function | L3 |
| Debug missing/non-progressing termination | Debug (Gỡ lỗi) | L4 |
| Nhận biết production risk do uncontrolled depth | Engineering reasoning | L4 |
16. Exit Criteria (Tiêu chí qua bài)
- [ ] Có thể vẽ stack growth và stack shrink của recursion 3–5 tầng mà không chạy code.
- [ ] Có thể chỉ ra local parameter của từng invocation cùng một function.
- [ ] Có thể xác định đúng base case và recursive step trong ít nhất 4/5 scenario mới.
- [ ] Có thể chứng minh recursive step đang tiến tới base case bằng một chuỗi state/input cụ thể.
- [ ] Có thể debug function có base case nhưng không reach được base case.
- [ ] Có thể giải thích
RangeError: Maximum call stack size exceededbằng stack growth mà không khẳng định một giới hạn frame cố định cho mọi runtime. - [ ] Có thể implement một recursive function nhỏ với domain/input contract rõ ràng.
- [ ] Có thể nêu khi maximum depth trở thành constraint production cần đánh giá.
17. Spiral Connection (Liên kết xoắn ốc)
Previous (Trước): Lesson 1.5.2 — Nested Calls đã dạy bạn trace caller → callee → resume và return-value flow. Recursion tái sử dụng đúng cơ chế đó, nhưng nhiều invocation có thể đến từ cùng một function definition.
Current (Hiện tại): Recursion làm rõ stack growth, local state theo từng invocation, base case và termination. Đây là bước đầu tiên bạn dùng Call Stack mental model để giải thích một failure mode trực tiếp: stack overflow.
Next (Tiếp theo): Lesson 1.5.4 — Exception & Stack Unwinding sẽ cho thấy frame có thể rời stack không chỉ qua return bình thường mà còn do exception propagation. Lesson 1.5.5 sẽ kết hợp Call Stack với scope, closure và value flow thành Full Execution Trace. Call Stack sau đó quay lại ở Stage 3 khi học Async / Event Loop và ở Stage 11 khi học memory/performance sâu hơn.