Chuyển đến nội dung chính (Skip to content)
Mục lục bài viết
Web Development Essentials

Web Development Essentials 034.3: Cấu trúc và Chức năng Điều khiển trong JavaScript

Web Development Essentials 034.3: Cấu trúc và Chức năng Điều khiển trong JavaScript

Tổ chức hàm JavaScript với if, switch, vòng lặp và return; hiểu truthy, so sánh, break, continue và xử lý Promise bằng async/await qua dữ liệu DEMO.

Ở bài trước, chương trình tính được valid nhưng vẫn tạo dữ liệu minh họa kể cả khi đầu vào sai. Trong ứng dụng thật, chỉ biết đúng/sai chưa đủ: cần quyết định nhánh nào được chạy, khi nào dừng và dữ liệu nào được trả lại. Một vòng lặp thiếu điều kiện kết thúc còn có thể giữ main thread bận liên tục, làm trang khó tương tác.

Bài 034.3, Cấu trúc và Chức năng Điều khiển trong JavaScript, tham chiếu Web Development Essentials 030-100 trang in 238–270, được viết mới và đối chiếu nguồn ngày 23/10/2026. Bạn cần hiểu binding, kiểu, array và object. Kết quả học tập là tổ chức kiểm tra đầu vào bằng hàm, chọn nhánh có chủ đích, duyệt danh sách có giới hạn và nhận biết đường thành công/thất bại của một tác vụ bất đồng bộ đơn giản.

Truthy và falsy không chỉ là khác không

Biểu thức trong if được chuyển thành boolean theo quy tắc ngôn ngữ. Các giá trị falsy thường gặp gồm false, 0, -0, 0n, chuỗi rỗng, null, undefined và NaN. Chuỗi “0”, chuỗi “false”, array rỗng và object rỗng đều truthy. Vì vậy, if (count) không thay thế kiểm tra count là số nguyên hợp lệ; if (items) cũng không chứng minh danh sách có phần tử.

Toán tử === và !== so sánh nghiêm ngặt, tránh chuyển kiểu như == hoặc !=. Số 2 không nghiêm ngặt bằng chuỗi “2”, trong khi so sánh lỏng có thể chuyển đổi và cho true. Object được so theo identity; hai object literal trông giống nhau vẫn không phải cùng một object. Chuẩn hóa đầu vào trước, rồi so kiểu và giá trị theo nhu cầu, thay vì dùng loose equality để che sự không nhất quán dữ liệu.

<, <=, > và >= kiểm tra quan hệ theo quy tắc của toán hạng. Hai chuỗi có thể được so theo thứ tự chuỗi, chẳng hạn “10” nhỏ hơn “2” trong phép so chuỗi đó; đừng suy ra kết quả so số. NaN làm các phép so quan hệ thông thường không cho thứ tự số bạn mong đợi. Dùng dấu ngoặc để ý định rõ khi ghép nhiều điều kiện.

&& và || đánh giá ngắn mạch và trả về một giá trị toán hạng, không luôn trả boolean. Điều này hữu ích để tránh chạy vế phải khi không cần, nhưng dễ gây nhầm khi gán kết quả. Trong điều kiện, kết quả cuối vẫn được diễn giải theo truthiness. ! đảo giá trị boolean sau chuyển đổi. Khi cần kiểm tra cả hai điều kiện, dùng AND; khi chỉ cần một, dùng OR và thử trường hợp biên để xác nhận.

Chọn nhánh và dừng sớm

Dùng if/else if/else khi các điều kiện có thứ tự ưu tiên. Khi một nhánh đầu đã khớp, các nhánh else if phía sau trong cùng chuỗi không được xét tiếp. Nhiều if độc lập có thể chạy nhiều nhánh. Viết ngoặc nhọn nhất quán để tránh một câu lệnh mới vô tình nằm ngoài nhánh dự định.

Switch phù hợp khi phân loại một giá trị theo các case rõ ràng. Nó dùng so sánh nghiêm ngặt cho việc khớp case. Break thoát switch; nếu không có break hoặc cơ chế thoát khác, luồng có thể tiếp tục sang các mệnh đề tiếp theo. Gộp case có chủ đích thì hợp lý, còn rơi xuống nhánh do quên break là lỗi cần kiểm tra. Trong một hàm nhỏ, return trực tiếp ở mỗi case cũng có thể làm ý định rõ.

function labelFor(channel) {
  switch (channel) {
    case "mail": return "Email";
    case "chat": return "Chat nội bộ";
    default: return "Chưa hỗ trợ";
  }
}
console.log(labelFor("mail"), labelFor("sms"));

Đây chỉ là nhãn hiển thị cho mã kênh DEMO. Nó không xác minh người nhận, mở ứng dụng chat hoặc gửi thư. Tách phần chuyển dữ liệu thành nhãn khỏi tác vụ bên ngoài giúp thử nghiệm mà không tạo hiệu ứng phụ.

Hàm kiểm tra trả lỗi sớm hoặc giá trị đã chuẩn hóa, để caller chọn đường xử lý.
Hình 1. Hàm kiểm tra trả lỗi sớm hoặc giá trị đã chuẩn hóa, để caller chọn đường xử lý. Mở hình để xem chi tiết.

Hàm có đầu vào, đầu ra và giới hạn

Tham số là tên trong định nghĩa hàm; đối số là giá trị truyền khi gọi. JavaScript truyền giá trị. Khi đối số là object, giá trị truyền là tham chiếu tới object đó: sửa thuộc tính qua tham chiếu có thể tác động object gốc, nhưng gán lại tham số không thay binding của bên gọi. Đừng mô tả trường hợp này như khả năng tùy ý gán lại biến của caller bằng pass-by-reference.

Return kết thúc đường thực thi thông thường của hàm và đưa giá trị về caller. Hàm thông thường đi tới cuối mà không return giá trị cho undefined. Trong các cấu trúc như try/finally, khối finally còn có quy tắc thực thi riêng; vì vậy không biến câu “mọi thứ sau return không bao giờ chạy” thành quy tắc cho mọi cấu trúc. Lab dưới không cần finally.

Function declaration thường có thể được gọi trước vị trí khai báo trong scope tương ứng. Function expression gán qua const/let chỉ dùng được sau khi binding được khởi tạo. Nếu đổi thành var, gọi trước khi gán có thể tạo TypeError vì giá trị lúc đó không phải hàm, thay vì cùng ReferenceError như const còn trong temporal dead zone. Cách khai báo và scope đều quan trọng.

Hàm nên làm một việc có đầu ra giải thích được. Tách chuẩn hóa đầu vào khỏi cập nhật DOM hay gọi mạng giúp kiểm tra thuận tiện. Điều đó không tự bảo đảm chạy nhanh hoặc giảm bộ nhớ trong mọi chương trình; đo khi có vấn đề hiệu năng thật. Đệ quy có ích cho một số cấu trúc nhưng cần điều kiện dừng và xem xét độ sâu stack. Danh sách phẳng nhỏ trong lab dùng vòng lặp dễ kiểm soát hơn.

Vòng lặp phải có đường tiến tới kết thúc

For cổ điển gom bước khởi tạo, điều kiện và cập nhật; while phù hợp khi điều kiện dẫn đường. Điều kiện while không cần đổi giá trị boolean ở mọi vòng: nó có thể vẫn true nhiều lần rồi mới false, hoặc vòng kết thúc bằng break. Yêu cầu thực tế là có đường kết thúc đúng theo bài toán, không để một continue bỏ qua bước cập nhật cần thiết.

Break thoát vòng lặp gần nhất hoặc switch liên quan trong trường hợp không nhãn; continue bỏ phần còn lại của lượt hiện tại. Với for cổ điển, biểu thức cập nhật vẫn được chạy khi continue chuyển sang lượt tiếp. Với while, bạn phải bảo đảm biến tiến triển ở đường có continue. Không chạy một ví dụ vòng lặp vô hạn trong tab đang dùng để học.

for (const item of items) lấy các giá trị từ iterable như array. Const được khai báo cho từng lượt, tránh dựa vào tên chưa khai báo. for...in duyệt các khóa thuộc tính enumerable theo quy tắc của nó, không phải lựa chọn tương đương để lấy trực tiếp từng object trong array. Chọn dạng thể hiện đúng mục đích đọc mã.

Lab: dừng dữ liệu sai trước khi nhận yêu cầu

Chạy trong module hoặc context mới. Không có DOM, thư viện hoặc mạng. Hàm trả object kết quả, nên caller có thể xử lý lỗi mà không phải đoán từ một chuỗi log. Điều kiện typeof giới hạn đầu vào lab là chuỗi, như một giá trị form text; các kiểu khác bị từ chối rõ ràng.

function parseCount(raw) {
  if (typeof raw !== "string") {
    return { ok: false, reason: "type" };
  }
  const text = raw.trim();
  if (text === "") {
    return { ok: false, reason: "empty" };
  }
  const value = Number(text);
  if (!Number.isFinite(value) || !Number.isInteger(value)) {
    return { ok: false, reason: "integer" };
  }
  if (value < 1 || value > 20) {
    return { ok: false, reason: "range" };
  }
  return { ok: true, value: value };
}

const input = ["2", "", "21", "3", "DEMO", "4"];
const accepted = [];
for (const raw of input) {
  const result = parseCount(raw);
  if (!result.ok) {
    console.log("SKIP", result.reason);
    continue;
  }
  accepted.push(result.value);
  if (accepted.length === 2) {
    break;
  }
}
console.log("ACCEPTED", accepted.join(","));

Kết quả là SKIP empty, SKIP range và ACCEPTED “2,3”. Break dừng ngay sau khi nhận đủ hai mục, nên “DEMO” và “4” phía cuối chưa được xử lý trong vòng này. Đừng nhìn ba dòng log rồi kết luận mọi phần tử đều đã được kiểm tra. Nếu yêu cầu nghiệp vụ là xét tất cả, điều kiện break đó phải được bỏ hoặc thay bằng hành vi phù hợp.

Thử gọi parseCount riêng với null, khoảng trắng, “2.5”, “Infinity”, “0”, “20” và “ 2 ”. Kỳ vọng lần lượt là type, empty, integer, integer, range, thành công 20 và thành công 2. Việc không tạo accepted từ dữ liệu sai là thay đổi so với bài trước. Đây vẫn là validation trong bộ nhớ, chưa thay thế kiểm tra phía server khi có form thật.

Một while có giới hạn có thể diễn đạt số lượt thử DEMO: bắt đầu attempts bằng 0, lặp khi attempts nhỏ hơn 3, tăng attempts trước khi ghi log. Nó kết thúc với attempts bằng 3. Tăng trước giúp tránh quên bước tiến triển khi sau này thêm continue. Trong tác vụ mạng thực tế còn cần phân loại lỗi và backoff, không lặp ba lần ngay lập tức theo ví dụ đếm đơn giản này.

Break dừng sau hai mục hợp lệ; Promise có đường thành công và rejection được bắt riêng.
Hình 2. Break dừng sau hai mục hợp lệ; Promise có đường thành công và rejection được bắt riêng. Mở hình để xem chi tiết.

Phần mở rộng: Promise và async/await

Một hàm async trả Promise. Await chờ kết quả bất đồng bộ trong luồng async tương ứng; nó không biến mọi công việc nặng thành tác vụ nền và không tự ngăn khóa main thread do tính toán dài. Với một Promise bị reject, dùng try/catch ở nơi await để xử lý đường lỗi. Không bỏ qua rejection chỉ vì giao diện vẫn có nội dung cũ.

async function demoResult(fail) {
  if (fail) {
    throw new Error("DEMO chưa sẵn sàng");
  }
  return { status: "ready" };
}
async function runDemo() {
  try {
    const result = await demoResult(false);
    console.log("ASYNC", result.status);
    await demoResult(true);
  } catch (error) {
    console.log("HANDLED", error.message);
  }
}
await runDemo();

Đoạn cuối dùng top-level await nên chạy trong module hỗ trợ nó, không dán nguyên vào classic script rồi cho rằng mọi môi trường đều giống nhau. Kết quả mong đợi là ASYNC ready, sau đó HANDLED với thông điệp DEMO. Không có độ trễ mô phỏng hoặc request mạng ở đây; Promise chỉ giúp thấy hai đường kết quả trước khi áp dụng fetch ở bài sau.

Lab được chạy ngày 23/10/2026 bằng Node 22.22.2 và Chromium 151.0.7922.34. Ba khối mã cho kết quả mong đợi; module browser không có page error. Bảy trường hợp parseCount, truthiness, so chuỗi, OR trả giá trị, while ba lượt và sửa thuộc tính qua đối số được kiểm tra riêng. Chưa có request mạng, backend, worker hay benchmark đệ quy.

Tự kiểm tra

Vì sao array rỗng vẫn vào nhánh if? Một OR có trả chuỗi không? Tại sao break làm hai đầu vào cuối không được xét? Gán lại tham số object có thay biến bên gọi không? Vì sao async không bảo đảm mã CPU nặng chạy ở thread khác? Khi các câu trả lời gắn với đầu ra và phạm vi rõ ràng, bạn đã có nền tảng để nối hàm xử lý với sự kiện DOM.

Nguồn tham khảo

Bài liên quan trong bộ LPI

Tìm hiểu thêm