Lesson 0.4.5 — Loops
0. Metadata (Thông tin bài học)
| Field | Value |
|---|---|
| Stage | 0 — JavaScript Language Foundation |
| Module | 0.4 — Operators & Control Flow |
| Lesson | 0.4.5 — Loops |
| Competency | C01.4 — Control Flow |
| Depth | L3 (Use) |
| Prerequisites | 0.4.1 Operators, 0.4.2 Conditional Logic, 0.4.3 Short-circuit Evaluation, 0.4.4 Guard Clauses & Early Return |
| Estimated Cognitive Load | Medium |
1. Why This Exists (Vì sao cần học)
Bạn cần xử lý 100 đơn hàng từ API. Bạn viết:
processOrder(orders[0]);
processOrder(orders[1]);
processOrder(orders[2]);
// ... 97 dòng nữaĐiều này không scale. Bạn cần một cấu trúc nói với máy tính: "Làm điều này cho mỗi phần tử" — đó là vòng lặp (loop).
Nhưng không phải chỉ cần "viết for". Junior thường mắc phải:
- Off-by-one: lặp thiếu hoặc thừa một phần tử.
- Infinite loop: điều kiện dừng không bao giờ đạt được.
- Chọn sai loại vòng lặp: dùng
whilekhiforrõ ràng hơn, hoặc ngược lại.
Lesson này dạy bạn:
- Khi nào dùng
for,while,do...while. - Cách
breakvàcontinueđiều khiển luồng lặp. - Nhận diện và tránh các bug loop phổ biến.
Preview
Bạn sẽ học: for gom khởi tạo + điều kiện + cập nhật vào một dòng — giảm off-by-one. while phù hợp khi không biết trước số lần lặp. break thoát ngay, continue skip iteration.
2. Prerequisites (Yêu cầu đầu vào)
Trước khi học lesson này, bạn phải:
- [ ] Viết được
if/elsecơ bản (0.4.2). - [ ] Hiểu toán tử so sánh (
<,>,<=,>=,===) (0.4.1). - [ ] Hiểu
++và--(0.4.1). - [ ] Biết cách dùng guard clause (0.4.4).
Nếu thiếu một trong các kỹ năng trên, quay lại lesson tương ứng trước.
3. Learning Objectives (Mục tiêu học tập)
Sau lesson này, bạn có thể:
- Chọn đúng loại vòng lặp (
for,while,do...while) cho từng tình huống. - Dự đoán số lần lặp và giá trị biến đếm sau vòng lặp.
- Giải thích sự khác biệt giữa
breakvàcontinue. - Nhận diện off-by-one bug và infinite loop trong code.
- Refactor vòng lặp
whilethànhfor(và ngược lại) khi phù hợp.
4. Mental Model (Mô hình tư duy)
Hãy hình dung 3 loại vòng lặp như 3 cách đi qua một hành lang:
for : "Tôi đếm từng phòng, biết rõ có bao nhiêu phòng."
for (let i = 0; i < 10; i++)
// Khởi tạo → Kiểm tra → Thân → Cập nhật → Lặp lại
while : "Tôi đi cho đến khi gặp điều gì đó, không biết trước."
while (door.isOpen)
// Kiểm tra → Thân → Kiểm tra → Thân → ...
do...while : "Tôi bước vào phòng đầu tiên trước, rồi mới hỏi 'có tiếp không?'"
do { ... } while (condition)
// Thân → Kiểm tra → Thân → Kiểm tra → ...Điểm then chốt
Vòng lặp không phải là "cách lặp code". Nó là cấu trúc điều khiển luồng — bạn phải hiểu rõ:
- Điều kiện kiểm tra ở đâu? (trước thân hay sau thân)
- Biến nào thay đổi mỗi lần lặp?
- Điều kiện dừng có đảm bảo đạt được không?
5. Core Concepts (Các khái niệm cốt lõi)
Essential (Bắt buộc)
| Khái niệm | Khi nào dùng |
|---|---|
for | Biết trước số lần lặp, hoặc lặp qua index |
while | Không biết trước số lần, chỉ biết điều kiện dừng |
do...while | Cần chạy thân ít nhất một lần |
break | Thoát vòng lặp ngay lập tức |
continue | Bỏ qua phần còn lại của lần lặp hiện tại |
Supporting (Hỗ trợ)
- Loop invariant: điều kiện luôn đúng trong suốt vòng lặp.
- Termination condition: đảm bảo vòng lặp không vô hạn.
Awareness (Biết tồn tại)
for...ofvàfor...in(sẽ học ở 0.4.6).- Array methods (
forEach,map,filter) thay thế vòng lặp trong nhiều trường hợp (0.6.3).
Out of Scope (Không học trong bài này)
- Iterator protocol (Stage 2).
- Generator functions (Stage 2).
breakvới label (rất hiếm dùng, không cần ở L3).- Performance optimization của vòng lặp (Stage 11).
6. Worked Example (Ví dụ phân tích từng bước)
Ví dụ: Tìm số đầu tiên chia hết cho 7 trong mảng
Bước 1 — Phân tích yêu cầu:
- Input: mảng số.
- Output: số đầu tiên chia hết cho 7, hoặc
nullnếu không có. - Không biết trước số đầu tiên nằm ở đâu →
forqua index hoặcwhile.
Bước 2 — Chọn loại vòng lặp:
- Biết trước phạm vi (length của mảng) →
forphù hợp nhất. - Cần thoát sớm khi tìm thấy → dùng
break.
Bước 3 — Viết code:
function findDivisibleBy7(numbers) {
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] % 7 === 0) {
return numbers[i]; // Early return — thoát hàm luôn
}
}
return null;
}Bước 4 — Phân tích lựa chọn:
- Tại sao không
while? Vìforgom 3 phần (khởi tạo, điều kiện, cập nhật) vào một dòng, rõ ràng hơn cho lặp qua index. - Tại sao
returnthay vìbreak? Vìreturnthoát hàm luôn, ngắn gọn hơnbreak+returnsau loop.
Ví dụ 2: Đọc input đến khi hợp lệ
Bước 1 — Phân tích:
- Không biết trước cần hỏi bao nhiêu lần →
whilehoặcdo...while. - Cần hỏi ít nhất một lần →
do...whilephù hợp.
Bước 2 — Code:
function getValidInput() {
let input;
do {
input = prompt("Enter a number > 0:"); // `prompt` là Browser API, sẽ học ở Stage 4
} while (!input || Number(input) <= 0);
return Number(input);
}Bước 3 — Phân tích:
do...whileđảm bảo prompt xuất hiện ít nhất một lần.whilethuần cũng được, nhưng cần khởi tạoinputtrước — kém tự nhiên hơn.
Code Review Lens
Khi review code, nếu thấy while (true) hoặc for (;;) không có break rõ ràng, đây là red flag. Mọi vòng lặp phải có termination condition dễ nhìn thấy.
7. Prediction Exercise (Bài tập dự đoán)
WARNING
Đừng chạy code. Đoán output và giải thích flow.
Câu 1
let sum = 0;
for (let i = 1; i <= 5; i++) {
if (i === 3) continue;
sum += i;
}
console.log(sum);Dự đoán của bạn:
- Output?
i === 3có được cộng vàosumkhông?
Câu 2
let count = 0;
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (j === 1) break;
count++;
}
}
console.log(count);Dự đoán của bạn:
- Output?
breakthoát vòng lặp nào? (inner hay outer?)
Câu 3
let n = 5;
while (n > 0) {
console.log(n);
n--;
}Dự đoán của bạn:
- In ra những số nào?
ncuối cùng bằng bao nhiêu sau vòng lặp?
Câu 4 (Off-by-one trap)
const arr = [10, 20, 30];
for (let i = 0; i <= arr.length; i++) {
console.log(arr[i]);
}Dự đoán của bạn:
- Output?
- Dòng nào là bug?
[Đáp án & Giải thích]
Câu 1:
- Output:
12(1 + 2 + 4 + 5) i === 3không được cộng. Khii = 3,continuebỏ quasum += i, nhảy sangi = 4.
Câu 2:
- Output:
3 breakchỉ thoát vòng lặp trong (j). Mỗi lầnilặp,jchạy 0 →breakởj = 1. Vậy mỗi lầni,count++một lần.ichạy 3 lần →count = 3.
Câu 3:
- In ra:
5,4,3,2,1 ncuối cùng bằng0(vìn--chạy khin = 1, sau đón = 0, điều kiệnn > 0false, thoát).
Câu 4:
- Output:
10,20,30,undefined - Bug:
i <= arr.lengthnênichạy đến3, màarr[3]không tồn tại →undefined. Sửa thànhi < arr.length.
8. Implementation Lab (Bài lab thực hành)
Level 1 — Guided (Hướng dẫn)
Viết vòng lặp for in ra các số từ 1 đến 10:
for (/* khởi tạo */; /* điều kiện */; /* cập nhật */) {
console.log(/* biến đếm */);
}[Gợi ý]
for (let i = 1; i <= 10; i++) {
console.log(i);
}Level 2 — Partial Scaffold (Khung mẫu)
Hoàn thành hàm sumUntilNegative. Dùng while để cộng các số trong mảng cho đến khi gặp số âm đầu tiên (không cộng số âm).
function sumUntilNegative(numbers) {
let sum = 0;
let i = 0;
// Dùng while, dừng khi gặp số âm hoặc hết mảng
while (/* implement */) {
/* implement */
i++;
}
return sum;
}
// Test
console.log(sumUntilNegative([1, 2, 3, -1, 5])); // 6
console.log(sumUntilNegative([5, 10])); // 15
console.log(sumUntilNegative([-1, 2])); // 0[Đáp án tham khảo]
function sumUntilNegative(numbers) {
let sum = 0;
let i = 0;
while (i < numbers.length && numbers[i] >= 0) {
sum += numbers[i];
i++;
}
return sum;
}Level 3 — Independent (Độc lập)
Viết hàm findFirstPrime. Dùng for loop để tìm số nguyên tố đầu tiên trong mảng. Nếu không có, trả về null.
Yêu cầu:
- Không dùng
Array.prototypemethods (chỉ dùngfor,if). - Số nguyên tố là số > 1, chỉ chia hết cho 1 và chính nó.
- Dùng
breakhoặcreturnđể thoát sớm khi tìm thấy.
function isPrime(n) {
if (n <= 1) return false;
for (let i = 2; i < n; i++) {
if (n % i === 0) return false;
}
return true;
}
function findFirstPrime(numbers) {
// Implement here — dùng for loop, gọi isPrime
}
// Test
console.log(findFirstPrime([4, 6, 8, 9, 11, 12])); // 11
console.log(findFirstPrime([4, 6, 8])); // null[Đáp án tham khảo]
function findFirstPrime(numbers) {
for (let i = 0; i < numbers.length; i++) {
if (isPrime(numbers[i])) {
return numbers[i];
}
}
return null;
}9. Edge Cases (Các trường hợp ngoại lệ)
Edge 1: Infinite loop — quên cập nhật biến đếm
let i = 0;
while (i < 5) {
console.log(i);
// ❌ Quên i++
}Tại sao fail: i không bao giờ tăng, điều kiện i < 5 luôn đúng.
Fix: Luôn đảm bảo biến điều khiển vòng lặp thay đổi trong thân loop.
Edge 2: Off-by-one — dùng <= thay vì <
const arr = ["a", "b", "c"];
for (let i = 0; i <= arr.length; i++) {
console.log(arr[i]); // arr[3] → undefined
}Tại sao fail: arr.length là 3, index cuối là 2. i <= 3 chạy đến index 3 — ngoài phạm vi.
Fix: Dùng i < arr.length. Hoặc nếu cần <=, đảm bảo logic đúng (hiếm khi cần với array index).
Edge 3: break chỉ thoát vòng lặp gần nhất
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (j === 1) break; // Chỉ thoát vòng j
}
}Caveat: Nếu muốn thoát cả 2 vòng lặp, cần dùng flag hoặc return (nếu trong function).
Edge 4: continue trong while — quên cập nhật biến đếm
let i = 0;
while (i < 5) {
if (i === 2) continue; // ❌ i không tăng, vòng lặp vô hạn ở i = 2
console.log(i);
i++;
}Tại sao fail: Khi i === 2, continue nhảy lên kiểm tra điều kiện, nhưng i++ không chạy → i mãi là 2.
Fix: Đặt i++ trước continue, hoặc dùng for thay vì while.
10. Debug Lab (Bài lab gỡ lỗi)
Symptom (Triệu chứng): Hàm findIndexOf luôn trả về -1, dù phần tử tồn tại trong mảng.
Reproduction (Tái hiện lỗi):
function findIndexOf(arr, target) {
for (let i = 0; i < arr.length; i++) {
if (arr[i] === target) {
return i;
} else {
return -1;
}
}
}
console.log(findIndexOf(["a", "b", "c"], "b"));
// Actual: -1
// Expected: 1Evidence (Bằng chứng):
i = 0:arr[0]là"a", không phải"b"→ chạyelse→return -1ngay lập tức.- Vòng lặp không bao giờ đến
i = 1.
Hypothesis (Giả thuyết):return -1 nằm trong else của if bên trong vòng lặp. Nó nên nằm sau vòng lặp — chỉ trả về -1 khi đã duyệt hết mà không tìm thấy.
Verification (Xác minh):
function findIndexOf(arr, target) {
for (let i = 0; i < arr.length; i++) {
if (arr[i] === target) {
return i;
}
}
return -1; // Chỉ chạy khi loop kết thúc mà không tìm thấy
}
console.log(findIndexOf(["a", "b", "c"], "b"));
// → 1 ✅Root Cause (Nguyên nhân gốc rễ): Nhầm lẫn giữa "không khớp ở lần lặp này" và "không khớp trong cả mảng". return -1 trong else nghĩa là "phần tử đầu tiên không khớp → không có trong mảng", điều này logic sai.
Fix (Sửa lỗi):
return ikhi tìm thấy (bên trong loop).return -1sau loop (khi đã duyệt hết).- Không dùng
elsebên trong loop cho trường hợp này.
Prevention (Phòng ngừa):
- Khi viết search loop, luôn hỏi: "
return not-foundnên ở đâu?" → Đáp án: sau loop. - Viết test case với target ở vị trí đầu, giữa, cuối, và không tồn tại.
11. Design Exercise (Bài tập thiết kế giải pháp)
INFO
Ở depth L3, bài tập thiết kế tập trung vào quyết định đơn giản giữa các lựa chọn.
Context (Bối cảnh): Bạn cần viết hàm đọc dữ liệu từ API cho đến khi không còn trang nào nữa. Mỗi lần gọi trả về { data: [...], hasMore: boolean }.
Question:
- Bạn chọn
for,while, haydo...while? Giải thích. - Điều kiện dừng là gì?
- Biến nào thay đổi mỗi lần lặp?
[Đáp án tham khảo]
whilehoặcdo...while. Không biết trước có bao nhiêu trang.do...whilephù hợp hơn nếu chắc chắn có ít nhất một trang.whilephù hợp nếu có thể không có trang nào.- Điều kiện dừng:
!response.hasMore. - Biến thay đổi:
page(tăng mỗi lần) hoặchasMore(cập nhật từ response).
async function fetchAllPages(apiCall) {
const allData = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await apiCall(page);
allData.push(...response.data);
hasMore = response.hasMore;
page++;
}
return allData;
}12. Production Scenario (Tình huống thực tế)
Bối cảnh: Bạn review PR của đồng nghiệp. Họ viết hàm xử lý log:
function processLogs(logs) {
for (let i = 0; i <= logs.length; i++) {
if (logs[i].level === "error") {
return logs[i];
}
}
return null;
}Vấn đề: Production crash với Cannot read properties of undefined (reading 'level').
Câu hỏi:
- Bug ở đâu?
- Tại sao crash không xảy ra ở mọi lần chạy?
- Bạn sửa như thế nào?
[Đáp án tham khảo]
i <= logs.lengthnên lần lặp cuốii = logs.length,logs[logs.length]làundefined.- Nếu mảng rỗng,
i = 0,logs[0]làundefined→ crash ngay. Nếu mảng có phần tử và phần tử đầu là"error", hàmreturnsớm trước khi đến index ngoài phạm vi → không crash (bug ẩn). - Sửa thành
i < logs.length.
13. AI-assisted Exercise (Bài tập với AI)
Level B — Challenge (Thử thách)
- Tự viết trước: Viết một đoạn giải thích ngắn (3–5 câu) về tại sao
forphù hợp hơnwhilekhi lặp qua mảng có biết trước length. Tập trung vào readability và bug prevention. - Đưa cho AI review: Dùng prompt: "Review my explanation of when to use for vs while loops in JavaScript. Is it accurate? What's missing?"
- So sánh: AI có nhắc đến off-by-one không? AI có cảnh báo về infinite loop trong
whilekhông? - Verify: Kiểm tra lại bằng MDN — search "for statement" và "while statement".
Gợi ý
AI thường nói: "Use for when you know the count, while when you don't." Điều này đúng nhưng surface-level. Hãy kiểm tra xem AI có giải thích tại sao for giảm off-by-one (vì 3 phần gom lại một chỗ) hay không. AI cũng có thể không nhắc đến do...while.
[Đáp án tham khảo]
Bạn nghĩ:
forgom khởi tạo, điều kiện, cập nhật vào một dòng. Khi đọcfor (let i = 0; i < arr.length; i++), tôi thấy ngay cả 3 phần — khó quên cập nhật biến đếm.whiletách rờì khởi tạo và cập nhật ra xa nhau, dễ quêni++gây infinite loop. Với mảng có length biết trước,forlà lựa chọn an toàn hơn.AI trả lờì: "Use a
forloop when the number of iterations is known beforehand, and awhileloop when the condition depends on runtime state. Both are valid, butforis more concise for array iteration." — Đúng nhưng mơ hồ.So sánh: AI đúng về rule of thumb, nhưng có thể không nhắc đến:
- Mechanism:
forgom 3 phần giúp giảm cognitive load. - Off-by-one: AI hiếm khi nhấn mạnh
i < arr.lengthvsi <= arr.length. do...while: AI thường bỏ qua hoàn toàn.continuetrongwhile: AI hiếm khi cảnh báo về risk quên cập nhật biến đếm.
- Mechanism:
Điểm AI nói sai hoặc quá mơ hồ:
- "More concise" là subjective. Không giải thích tại sao concise lại tốt hơn (vì giảm working memory).
- AI có thể không nhắc đến việc
forgiúp scope biến đếm (let ichỉ tồn tại trong loop).
Kết luận: Nếu bạn chỉ ra được rằng
forgiảm bug bằng cách gom 3 phần vào một chỗ và scope biến đếm — bạn đã hiểu sâu hơn AI. Điều này sẽ quay lại ở Stage 2 (Iterator protocol), Stage 6 (TypeScript for-of type narrowing), và Stage 11 (Performance — loop unrolling).
14. Teach Back (Dạy lại)
Yêu cầu: Giải thích cho một đồng nghiệp Junior trong 2 phút:
"Tại sao đoạn code
for (let i = 0; i <= arr.length; i++)có thể gây crash production? Và tại saoforthường an toàn hơnwhilekhi lặp qua mảng?"
Dùng đúng terminology: off-by-one, termination condition, infinite loop, index out of bounds.
Mô phỏng
- Bạn nói:
arr.lengthlà số phần tử, nhưng index bắt đầu từ0. Nếu mảng có 3 phần tử, index hợp lệ là0,1,2.arr.lengthlà3, nênarr[3]làundefined. Nếu code sau đó đọcarr[3].property, sẽ crash với "cannot read property of undefined".foran toàn hơnwhilevì nó gom khởi tạo, điều kiện, và cập nhật biến đếm vào một dòng. Vớiwhile, bạn có thể viếti++ở dưới cùng, nhưng nếu quên hoặc viết nhầm điều kiện, bạn có infinite loop.forbuộc bạn nghĩ về cả 3 phần ngay từ đầu.
💡 Hãy tưởng tượng vòng lặp như một chiếc xe.
forlà xe tự động — bạn chỉ cần đặt điểm đến.whilelà xe số sàn — bạn phải tự đổi số (cập nhật biến) nếu không muốn động cơ nổ tung (infinite loop).
Gợi ý đánh giá bản thân
- Đồng nghiệp có hiểu tại sao
<=gây lỗi với array index không? - Bạn có thể chỉ ra một bug off-by-one trong codebase của bạn không?
- Bạn có thể giải thích tại sao
continuetrongwhiledễ gây infinite loop hơn trongforkhông?
15. Assessment (Đánh giá)
| Objective (Mục tiêu) | Hình thức đánh giá | Tiêu chí |
|---|---|---|
| Chọn đúng loại vòng lặp | Decision (Quyết định) | Chọn đúng ≥ 3/4 scenarios |
| Dự đoán số lần lặp | Prediction (Dự đoán) | Đúng ≥ 8/10 cases |
Giải thích break vs continue | Explain (Giải thích) | Phân biệt đúng scope và behavior |
| Nhận diện off-by-one / infinite loop | Debug (Gỡ lỗi) | Tìm ra ≥ 2/3 bugs |
Refactor while ↔ for | Implementation (Thực hành) | Code tương đương, rõ ràng hơn |
16. Exit Criteria (Tiêu chí qua bài)
- [ ] Có thể chọn đúng loại vòng lặp (
for,while,do...while) cho từng tình huống. - [ ] Có thể dự đoán đúng số lần lặp và giá trị biến sau vòng lặp trong 8/10 trường hợp.
- [ ] Có thể giải thích sự khác biệt giữa
break(thoát loop) vàcontinue(skip iteration). - [ ] Có thể nhận diện off-by-one bug và infinite loop trong code thực tế.
- [ ] Có thể refactor vòng lặp
whilethànhfor(và ngược lại) mà không thay đổi behavior.
17. Spiral Connection (Liên kết xoắn ốc)
Previous (Trước): Conditional Logic (0.4.2) → Guard Clauses & Early Return (0.4.4)
Current (Hiện tại): Loops —
for,while,do...while,break,continue.Next (Tiếp theo):
for...ofvsfor...in(0.4.6) → Array Iteration methods (0.6.3) → Iterator Protocol (Stage 2) → Async iteration (Stage 3) → Performance — loop unrolling (Stage 11)