Skip to content

Lesson 0.4.5 — Loops ​

Bài 0.4.5 — Loops
for, while, do...while, break, continue • 20 phút
0:00 / 0:00

0. Metadata (Thông tin bài học) ​

FieldValue
Stage0 — JavaScript Language Foundation
Module0.4 — Operators & Control Flow
Lesson0.4.5 — Loops
CompetencyC01.4 — Control Flow
DepthL3 (Use)
Prerequisites0.4.1 Operators, 0.4.2 Conditional Logic, 0.4.3 Short-circuit Evaluation, 0.4.4 Guard Clauses & Early Return
Estimated Cognitive LoadMedium

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:

js
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 while khi for rõ 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 break và 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/else cơ 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ể:

  1. Chọn đúng loại vòng lặp (for, while, do...while) cho từng tình huống.
  2. Dự đoán số lần lặp và giá trị biến đếm sau vòng lặp.
  3. Giải thích sự khác biệt giữa break và continue.
  4. Nhận diện off-by-one bug và infinite loop trong code.
  5. Refactor vòng lặp while thành for (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:

text
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ệmKhi nào dùng
forBiết trước số lần lặp, hoặc lặp qua index
whileKhông biết trước số lần, chỉ biết điều kiện dừng
do...whileCần chạy thân ít nhất một lần
breakThoát vòng lặp ngay lập tức
continueBỏ 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...of và 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).
  • break vớ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 null nếu không có.
  • Không biết trước số đầu tiên nằm ở đâu → for qua index hoặc while.

Bước 2 — Chọn loại vòng lặp:

  • Biết trước phạm vi (length của mảng) → for phù hợp nhất.
  • Cần thoát sớm khi tìm thấy → dùng break.

Bước 3 — Viết code:

js
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ì for gom 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 return thay vì break? Vì return thoát hàm luôn, ngắn gọn hơn break + return sau 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 → while hoặc do...while.
  • Cần hỏi ít nhất một lần → do...while phù hợp.

Bước 2 — Code:

js
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.
  • while thuần cũng được, nhưng cần khởi tạo input trướ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 ​

js
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 === 3 có được cộng vào sum không?

Câu 2 ​

js
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?
  • break thoát vòng lặp nào? (inner hay outer?)

Câu 3 ​

js
let n = 5;
while (n > 0) {
  console.log(n);
  n--;
}

Dự đoán của bạn:

  • In ra những số nào?
  • n cuối cùng bằng bao nhiêu sau vòng lặp?

Câu 4 (Off-by-one trap) ​

js
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 === 3 không được cộng. Khi i = 3, continue bỏ qua sum += i, nhảy sang i = 4.

Câu 2:

  • Output: 3
  • break chỉ thoát vòng lặp trong (j). Mỗi lần i lặp, j chạy 0 → break ở j = 1. Vậy mỗi lần i, count++ một lần. i chạy 3 lần → count = 3.

Câu 3:

  • In ra: 5, 4, 3, 2, 1
  • n cuối cùng bằng 0 (vì n-- chạy khi n = 1, sau đó n = 0, điều kiện n > 0 false, thoát).

Câu 4:

  • Output: 10, 20, 30, undefined
  • Bug: i <= arr.length nên i chạy đến 3, mà arr[3] không tồn tại → undefined. Sửa thành i < 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:

js
for (/* khởi tạo */; /* điều kiện */; /* cập nhật */) {
  console.log(/* biến đếm */);
}
[Gợi ý]
js
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).

js
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]
js
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.prototype methods (chỉ dùng for, if).
  • Số nguyên tố là số > 1, chỉ chia hết cho 1 và chính nó.
  • Dùng break hoặc return để thoát sớm khi tìm thấy.
js
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]
js
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 ​

js
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ì < ​

js
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 ​

js
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 ​

js
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):

js
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: 1

Evidence (Bằng chứng):

  • i = 0: arr[0] là "a", không phải "b" → chạy else → return -1 ngay 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):

js
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 i khi tìm thấy (bên trong loop).
  • return -1 sau loop (khi đã duyệt hết).
  • Không dùng else bê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-found nê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:

  1. Bạn chọn for, while, hay do...while? Giải thích.
  2. Điều kiện dừng là gì?
  3. Biến nào thay đổi mỗi lần lặp?
[Đáp án tham khảo]
  1. while hoặc do...while. Không biết trước có bao nhiêu trang. do...while phù hợp hơn nếu chắc chắn có ít nhất một trang. while phù hợp nếu có thể không có trang nào.
  2. Điều kiện dừng: !response.hasMore.
  3. Biến thay đổi: page (tăng mỗi lần) hoặc hasMore (cập nhật từ response).
js
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:

js
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:

  1. Bug ở đâu?
  2. Tại sao crash không xảy ra ở mọi lần chạy?
  3. Bạn sửa như thế nào?
[Đáp án tham khảo]
  1. i <= logs.length nên lần lặp cuối i = logs.length, logs[logs.length] là undefined.
  2. 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àm return sớm trước khi đến index ngoài phạm vi → không crash (bug ẩn).
  3. Sửa thành i < logs.length.

13. AI-assisted Exercise (Bài tập với AI) ​

Level B — Challenge (Thử thách)

  1. 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 for phù hợp hơn while khi lặp qua mảng có biết trước length. Tập trung vào readability và bug prevention.
  2. Đư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?"
  3. So sánh: AI có nhắc đến off-by-one không? AI có cảnh báo về infinite loop trong while không?
  4. 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ĩ: for gom khởi tạo, điều kiện, cập nhật vào một dòng. Khi đọc for (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. while tách rờì khởi tạo và cập nhật ra xa nhau, dễ quên i++ gây infinite loop. Với mảng có length biết trước, for là lựa chọn an toàn hơn.

  • AI trả lờì: "Use a for loop when the number of iterations is known beforehand, and a while loop when the condition depends on runtime state. Both are valid, but for is 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: for gom 3 phần giúp giảm cognitive load.
    • Off-by-one: AI hiếm khi nhấn mạnh i < arr.length vs i <= arr.length.
    • do...while: AI thường bỏ qua hoàn toàn.
    • continue trong while: AI hiếm khi cảnh báo về risk quên cập nhật biến đếm.
  • Đ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 for giúp scope biến đếm (let i chỉ tồn tại trong loop).
  • Kết luận: Nếu bạn chỉ ra được rằng for giả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 sao for thường an toàn hơn while khi 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.length là 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.length là 3, nên arr[3] là undefined. Nếu code sau đó đọc arr[3].property, sẽ crash với "cannot read property of undefined". for an toàn hơn while vì 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ới while, bạn có thể viết i++ ở 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. for buộ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. for là xe tự động — bạn chỉ cần đặt điểm đến. while là 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 continue trong while dễ gây infinite loop hơn trong for khô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ặpDecision (Quyết định)Chọn đúng ≥ 3/4 scenarios
Dự đoán số lần lặpPrediction (Dự đoán)Đúng ≥ 8/10 cases
Giải thích break vs continueExplain (Giải thích)Phân biệt đúng scope và behavior
Nhận diện off-by-one / infinite loopDebug (Gỡ lỗi)Tìm ra ≥ 2/3 bugs
Refactor while ↔ forImplementation (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 while thành for (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...of vs for...in (0.4.6) → Array Iteration methods (0.6.3) → Iterator Protocol (Stage 2) → Async iteration (Stage 3) → Performance — loop unrolling (Stage 11)

📴 Offline Mode — Content served from cache