← Lập trình JavaScript cơ bản

Bài 3 · Cơ bản · 20 phút

Rẽ nhánh & vòng lặp

Biên soạn bởi Nguyễn Anh Tuấn

Điều khiển luồng (control flow) trong JavaScript: if/else, switch, toán tử logic &&/||/??, vòng lặp for/while, break/continue và for...of - kèm bẫy kinh điển.

Chương trình ở Bài 1-2 chạy thẳng một mạch từ trên xuống. Bài này thêm bộ công cụ điều khiển luồng (control flow): rẽ nhánh - chọn việc theo điều kiện, và vòng lặp - chạy lại một khối nhiều lần. Với if, điều kiện là biểu thức được quy về đúng/sai theo luật truthy/falsy ở bài “Biến & kiểu dữ liệu JavaScript” - và so sánh thì luôn dùng ===:

nhánh thử từ trên xuống - và bẫy một dấu =

const diem = 8.5;

if (diem >= 8) {
  console.log("Gioi");
} else if (diem >= 6.5) {
  console.log("Kha");   // 8.5 cung thoa - nhung nhanh TREN chay truoc
} else {
  console.log("Chua dat");
}

let x = 0;
if (x = 5) {            // GAN, khong phai so sanh - 5 truthy nen LUON chay
  console.log("x bay gio la", x);
}

Kết quả khi chạy

Gioi
x bay gio la 5

Các nhánh được thử từ trên xuống và chỉ nhánh đúng đầu tiên chạy - đặt >= 6.5 lên trước là điểm 9 cũng thành “Kha”, nên thứ tự nhánh là một quyết định. Khối dưới là bẫy kinh điển: phép gán trả về giá trị vừa gán, nên x = 5 là biểu thức hợp lệ ra 5 - truthy - nhánh luôn chạy còn x bị ghi đè, không một lời báo lỗi. Gặp if “luôn chạy” khó hiểu: soi dấu = trước tiên.

  • Điều kiện quy về đúng/sai theo truthy/falsy (Bài 2); so sánh luôn dùng ===.
  • Nhánh thử từ trên xuống, chạy nhánh đúng ĐẦU TIÊN - thứ tự nhánh quan trọng.
  • if (x = 5) là GÁN: biểu thức ra 5 (truthy) nên nhánh luôn chạy - không hề báo lỗi.

Ghép điều kiện bằng && (và), || (hoặc), ! (phủ định). Hai điều đáng nhớ: chúng trả về một trong hai vế chứ không ép ra true/false, và chúng ngắn mạch (short-circuit) - vế trái đủ quyết định thì vế phải không được tính. Cộng thêm toán tử 3 ngôi ? : - một if/else nằm gọn trong biểu thức:

short-circuit, ?: và ?? - khi 0 là dữ liệu thật

const dangNhap = true;
dangNhap && console.log("Chao mung quay lai");   // "neu ... thi ..."

const diem = 9;
console.log(diem >= 5 ? "Dau" : "Truot");   // toan tu 3 ngoi

const soLanThu = 0;              // 0 la du lieu THAT
console.log(soLanThu || 3);      // || thay nham ca 0 va ""
console.log(soLanThu ?? 3);      // ?? chi thay null/undefined

Kết quả khi chạy

Chao mung quay lai
Dau
3
0

Toán tử mới ?? (nullish coalescing) chỉ coi null/undefined là “chưa có”. Khác biệt với ||: 0"" là falsy thật nhưng nhiều khi là dữ liệu hợp lệ - điểm 0 vẫn là điểm.

  • && và || ngắn mạch: vế phải chỉ chạy khi cần - đọc a && f() là “nếu a thì gọi f”.
  • Toán tử 3 ngôi ?: là if/else dạng biểu thức - hợp phép chọn ngắn, đừng lồng nhiều tầng.
  • Mặc định cho dữ liệu có thể là 0/"" → dùng ??; còn || sẽ ghi đè nhầm chúng.

Khi so một giá trị với nhiều khả năng cụ thể, chuỗi else-if dài khó đọc - switch sinh ra cho việc đó. Nó so sánh nghiêm ngặt bằng ===, chạy từ case khớp tới khi gặp break, còn default hứng phần còn lại. Hai case viết liền nhau là cách gộp case cố ý:

điều khiển nhân vật bằng phím

const phim = "w";
switch (phim) {
  case "w":
    console.log("Di len");
    break;
  case "a":
  case "d":   // gop 2 case = fall-through CO Y
    console.log("Di ngang");
    break;
  default:
    console.log("Phim khong ho tro");
}

Kết quả khi chạy

Di len

Còn quên break là dính fall-through (chảy xuyên) ngoài ý muốn: chạy xong một case, máy chạy tiếp case bên dưới mà không so sánh gì nữa:

quên break - chạy xuyên xuống case dưới

const mon = "toan";
switch (mon) {
  case "toan":
    console.log("Hoc toan");   // quen break o day...
  case "tin":
    console.log("Hoc tin");    // ...nen chay xuyen xuong day
}

Kết quả khi chạy

Hoc toan
Hoc tin
  • switch so sánh === - switch ("1") không khớp case 1.
  • Mỗi nhóm case kết thúc bằng break; quên break là fall-through ngoài ý muốn.
  • So một biến với danh sách giá trị cụ thể → switch; điều kiện khoảng (diem >= 8) → else if.

Vòng lặp chạy lại một khối khi điều kiện còn đúng. Câu lệnh for gói 4 mảnh: khởi tạo (chạy đúng một lần) → điều kiện (kiểm TRƯỚC mỗi vòng) → thâncập nhật. Bấm Bước để xem máy đi qua từng mảnh - đây chính là thứ một debugger cho bạn thấy:

Cộng dồn 1..n với n =
1 let tong = 0;
2 for (let i = 1; i <= 3; i++) {
3 tong += i;
4 }
biến đếm i
1
điều kiện i <= 3
1 <= 3
tong
0
Khởi tạo tong = 0 và i = 1 - phần khởi tạo chỉ chạy một lần.
bước 1/12 · thân đã chạy 0 lần

Hai quan sát: thân có thể chạy 0 lần (thử n = 0), và vòng lặp chỉ dừng khi điều kiện sai. while là for chỉ giữ lại phần điều kiện - hợp khi chưa biết trước số vòng; còn do...while kiểm tra SAU nên thân chạy ít nhất một lần:

while: lặp đến khi đạt mục tiêu

let von = 100;
let nam = 0;
while (von < 200) {     // bao gio von gap doi? chua biet truoc so vong
  von = von * 1.07;     // lai kep 7%/nam
  nam = nam + 1;
}
console.log(nam, "nam ->", von.toFixed(2));

Kết quả khi chạy

11 nam -> 210.49

Trong thân có hai lệnh điều khiển: break dừng hẳn vòng lặp, còn continue bỏ phần còn lại của vòng hiện tại, nhảy sang vòng kế:

continue bỏ qua một vòng · break dừng hẳn

const giu = [];
for (let i = 1; i <= 10; i++) {
  if (i % 2 === 0) continue;   // chan -> bo qua, sang vong ke
  if (i > 7) break;            // qua 7 -> dung han vong lap
  giu.push(i);
}
console.log(giu);

Kết quả khi chạy

[ 1, 3, 5, 7 ]

Bẫy 1 - vòng lặp vô tận

Quên i++ trong thân while (i <= 5) là điều kiện không bao giờ sai - chương trình treo, phải đóng tab hoặc Ctrl+C. Trước khi chạy, tự hỏi: biến trong điều kiện có tiến về phía làm điều kiện sai không?

Bẫy thứ hai tinh vi hơn - off-by-one (lệch một đơn vị): nhầm giữa <<= làm vòng lặp chạy thừa hoặc thiếu đúng một lần:

một dấu = lệch một vòng

let lan1 = 0, lan2 = 0;
for (let i = 0; i < 5; i++) lan1++;    // i = 0..4 -> 5 vong
for (let i = 0; i <= 5; i++) lan2++;   // i = 0..5 -> 6 vong
console.log(lan1, lan2);

Kết quả khi chạy

5 6
  • for = khởi tạo → điều kiện → thân → cập nhật; kiểm TRƯỚC nên thân có thể chạy 0 lần.
  • Biết trước số vòng → for; chưa biết → while; cần chạy ít nhất một lần → do...while.
  • break dừng hẳn; continue sang vòng kế. Hai bẫy phải soi: lặp vô tận và off-by-one.

Duyệt mảng bằng for + chỉ số vẫn được, nhưng phải nuôi biến đếm và dễ off-by-one. Cách hiện đại: for...of lấy thẳng từng giá trị - mảng hay chuỗi đều duyệt được. Đừng nhầm với for...in: nó lấy key, mà key của mảng lại là chuỗi - nó sinh ra cho object, không nên dùng với mảng:

of lấy GIÁ TRỊ · in lấy KEY (chuỗi)

const dsMeo = ["Mun", "Xam"];

for (const ten of dsMeo) {   // of: lay GIA TRI
  console.log("meo", ten);
}

for (const k in dsMeo) {     // in: lay KEY - va key la CHUOI
  console.log(k, typeof k);
}

Kết quả khi chạy

meo Mun
meo Xam
0 string
1 string

Key dạng chuỗi cộng với chỉ số là dính ngay bẫy nối chuỗi của Bài 2. Một tin tốt: sang bài “Mảng & object” (bài 5) bạn sẽ gặp map/filter - nhiều vòng lặp viết tay hôm nay sẽ tự biến mất.

  • for...of duyệt GIÁ TRỊ của mảng/chuỗi - không biến đếm, không off-by-one.
  • for...in duyệt KEY dạng chuỗi, sinh ra cho object - tránh dùng cho mảng.
  • Cần cả chỉ số lẫn giá trị → for...of với entries(), gặp ở bài Mảng & object.

Tiếp theo

Đã nắm đủ rẽ nhánh và vòng lặp - bộ xương của mọi chương trình. Bài sau - “Hàm, closure & this” - gói khối lệnh thành thứ tái dùng được: hàm là công dân hạng nhất, closure - và một bug kinh điển khi closure gặp đúng vòng lặp for bạn vừa học.

Câu hỏi thường gặp

Bằng === - nghiêm ngặt, không ép kiểu. Vì vậy switch ("1") KHÔNG khớp case 1: chuỗi khác số là trượt luôn, không có chuyện "1" được tự đổi thành 1. Đây là điểm cộng của switch: nó tránh sẵn các bẫy ép kiểu của == đã học ở Bài 2.

Biết trước số vòng (đếm 1..n, duyệt hết một mảng) → for hoặc for...of. Chưa biết trước, chỉ biết "lặp đến khi đạt điều kiện" (chờ nhập đúng mật khẩu, chờ vốn gấp đôi) → while. Còn do...while dành cho việc phải làm ÍT NHẤT một lần rồi mới biết có làm tiếp không, ví dụ hỏi "chơi ván nữa không?".

Không hỏng máy - nhưng tab trình duyệt hoặc process Node sẽ đứng hình vì bận chạy mãi không thoát. Trình duyệt: đóng tab; Node: nhấn Ctrl+C trong terminal. Phòng tránh: nhìn thân vòng lặp và tự hỏi "biến trong điều kiện CÓ tiến về phía làm điều kiện sai không?" - nếu không, đó là vòng lặp vô tận.

Gần như nguyên xi: JS thừa hưởng cú pháp for/while/do...whilebreak/continue từ C. Hai khác biệt đáng nhớ: JS có thêm for...of, và let trong for tạo một biến MỚI cho mỗi vòng - chi tiết này thành quan trọng khi gặp closure ở bài 4.

So sánh với bài "Vòng lặp: for, while, do-while" - khoá C cơ bản →

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

Mèo con viết let x = 0; rồi if (x = 5) { console.log("vao nhanh"); }. Chuyện gì xảy ra?

  1. 1

    FizzBuzz kiểu mèo

    In các số 1-20: chia hết cho 3 in "Meo", chia hết cho 5 in "Gru", chia hết cả hai in "MeoGru", còn lại in chính số đó.

    Hoàn thành khi: 3 → Meo, 5 → Gru, 15 → MeoGru, 20 → Gru. Nhánh "chia hết cả hai" phải đứng TRÊN CÙNG - đặt sau là 15 chỉ ra Meo.

  2. 2

    ?? vs ||

    Đoán trước rồi chạy: 0 || 5, 0 ?? 5, "" || "meo", "" ?? "meo", null ?? "meo".

    Hoàn thành khi: Ra 5, 0, "meo", "", "meo" - || thay cả 0 lẫn ""; ?? chỉ thay null/undefined.

  3. 3

    switch ngày trong tuần

    Viết switch (ngay): các chuỗi "t2""t6" in "Ngay di hoc", gộp "t7""cn" in "Cuoi tuan", default in "Khong hop le".

    Hoàn thành khi: "t4" → Ngay di hoc; "cn" → Cuoi tuan; "t9" → Khong hop le. Mỗi nhóm case kết thúc bằng break.

  4. 4

    Bảng cửu chương

    Dùng for in bảng cửu chương 7, dạng 7 x 1 = 77 x 10 = 70.

    Hoàn thành khi: Đúng 10 dòng, không thừa không thiếu; dòng cuối là 7 x 10 = 70.

  5. 5

    Thân chạy 0 lần

    Dùng công cụ ở Bước 4 với n = 0: thân vòng lặp chạy mấy lần? Vì sao?

    Hoàn thành khi: 0 lần - lần kiểm tra đầu tiên 1 <= 0 đã sai nên nhảy thẳng tới kết thúc; tong giữ nguyên 0.

  6. 6

    Sửa vòng lặp treo

    Đoạn let i = 1; while (i <= 5) { console.log(i); } chạy mãi không dừng. Mèo con sửa để nó in 1-5 rồi thoát.

    Hoàn thành khi: Thêm i++ vào thân: in 1 2 3 4 5 (mỗi số một dòng) rồi chương trình tự kết thúc, không cần Ctrl+C.