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.1: Cú pháp và Việc Thực thi trong JavaScript

Web Development Essentials 034.1: Cú pháp và Việc Thực thi trong JavaScript

Chạy JavaScript trong HTML và Console; hiểu script, defer, async, module, comment và ASI; phân biệt lỗi tải, cú pháp và thực thi qua lab DEMO cục bộ.

Một trang có nút nhưng nhấn không có phản hồi. Người học sửa CSS vì nút trông mờ, trong khi tệp JavaScript thực tế trả 404. Trường hợp khác, script đã tải nhưng đọc phần tử trước khi parser tạo nó. Muốn phân biệt các nguyên nhân này, cần tách ba câu hỏi: mã có được lấy về, có được thực thi và có chạy đúng thời điểm hay không?

Bài 034.1, Cú pháp và Việc Thực thi trong JavaScript, dựa trên khung Web Development Essentials 030-100, trang in 208–220. Nội dung viết mới, cập nhật nguồn ngày 23/10/2026. Bạn cần biết cấu trúc HTML, đường dẫn và HTTP cơ bản. Sau bài học, bạn có thể nối một tệp script với trang, đọc Console, viết câu lệnh cùng comment và giải thích khác biệt giữa classic script, defer, async và module ở mức nhập môn.

Ngôn ngữ và môi trường thực thi

JavaScript là ngôn ngữ được chuẩn hóa qua ECMAScript. Browser cung cấp thêm các API như document, sự kiện và thao tác mạng. Node.js cũng chạy JavaScript nhưng không tự có DOM của một trang web. Một đoạn tính tổng có thể chạy ở cả hai, còn document.querySelector cần môi trường có document thích hợp. Khi báo lỗi, ghi rõ đang chạy trong Console của trang hay terminal Node.

HTML mô tả nội dung, CSS định hình trình bày, JavaScript thêm hành vi. Ranh giới này giúp tìm lỗi nhưng không có nghĩa mỗi lớp hoạt động độc lập hoàn toàn: script có thể đổi DOM hoặc class; cấu trúc DOM và thời điểm chạy ảnh hưởng kết quả. Với chức năng chính, nên giữ nội dung nền tảng có ích ngay cả khi script thất bại.

Mã JavaScript được gửi xuống browser là mã người truy cập có thể đọc và thay đổi trên máy họ. Không đặt mật khẩu, API token hoặc quy tắc phân quyền duy nhất trong script client. Một hằng số đặt tên secret vẫn công khai nếu nằm trong tệp tải về. Các bài server sau sẽ xử lý ranh giới tin cậy này; ở đây chỉ dùng chuỗi DEMO.

Đưa mã vào HTML

Với classic script, có thể viết nội tuyến giữa cặp thẻ script hoặc dùng thuộc tính src tới tệp ngoài. Khi có src, đừng đặt thêm mã thực thi giữa hai thẻ để làm phương án dự phòng. Nội dung đó không chạy như một script thứ hai. Thẻ script cũng cần thẻ đóng; nó không phải void element như img.

<script>
  console.log("DEMO nội tuyến");
</script>
<script src="./classic.js" defer></script>

Không cần viết type cho classic JavaScript thông thường. Khi server trả tệp, Content-Type của phản hồi là vấn đề khác với thuộc tính type trong HTML; nên phục vụ JavaScript bằng MIME thích hợp như text/javascript. Một URL kết thúc .js nhưng trả trang HTML lỗi không trở thành JavaScript hợp lệ.

Phần mở rộng thực tế của chuỗi bài là ES modules. Dùng <script type="module" src="./app.js"></script> để khai báo module; tệp có thể import/export các thành phần. Module có phạm vi riêng và dùng strict mode tự động. Khai báo ở mức trên cùng trong module không tự thành thuộc tính window. Bài đầu chỉ dùng một module để quan sát phạm vi, chưa cần công cụ build hoặc package manager.

Thời điểm tải và chạy

Cách đưa script Hành vi cần nhớ trong trường hợp thông thường
Classic ngoài, không async/defer Parser dừng tại vị trí liên quan trong khi lấy và thực thi script
Classic ngoài với defer Tải song song với parse; chạy sau khi tài liệu được parse, theo thứ tự tài liệu
Classic ngoài với async Chạy khi sẵn sàng; không dựa vào thứ tự các thẻ để phối hợp phụ thuộc
Module không async Mặc định có hành vi trì hoãn tương tự defer, cùng việc lấy phụ thuộc module

Defer không có tác dụng hữu ích như vậy với classic script nội tuyến. Với module, không cần thêm defer để đạt hành vi mặc định. Async và defer không phải hai mức tăng tốc cứ ghép càng nhiều càng tốt. Nếu tệp phụ thuộc một tệp khác hoặc cần DOM sẵn, chọn cơ chế có quan hệ rõ ràng thay vì dựa vào lần tải thử nhanh trên máy mình.

DOMContentLoaded là mốc tài liệu đã parse và các script defer/module liên quan đã được xử lý theo quy tắc của chúng; nó không có nghĩa mọi ảnh đều tải xong, cũng không phải thời điểm browser mới bắt đầu vẽ trang. Một script đặt cuối body vẫn có thể chiếm main thread và làm tương tác chậm. Đặt cuối không bảo đảm mã nặng sẽ không gây giật.

Các tình huống có module bất đồng bộ, top-level await hoặc script chèn động có thêm chi tiết. Bài này chưa cần dùng chúng. Điều cần giữ là không đồng nhất tải tệp, thực thi mã, hoàn thành parse và render thành một sự kiện duy nhất.

Tách tải tài nguyên, phân tích cú pháp và thực thi để khoanh vùng lỗi JavaScript.
Hình 1. Tách tải tài nguyên, phân tích cú pháp và thực thi để khoanh vùng lỗi JavaScript. Mở hình để xem chi tiết.

Câu lệnh, tên và comment

Một câu lệnh như console.log("DEMO"); yêu cầu môi trường ghi thông tin vào Console. Dấu chấm phẩy kết thúc nhiều loại câu lệnh; không phải mọi cấu trúc JavaScript đều cần một dấu chấm phẩy sau dấu ngoặc đóng. Dùng định dạng nhất quán để người mới đọc được ranh giới, sau này có thể để formatter hỗ trợ.

JavaScript có cơ chế Automatic Semicolon Insertion, thường viết tắt ASI. Hai lệnh console.log trên hai dòng có thể hợp lệ dù thiếu dấu chấm phẩy. Tuy nhiên xuống dòng không luôn kết thúc câu lệnh theo ý người viết; một dòng bắt đầu bằng dấu ngoặc có thể tiếp nối biểu thức trước. Vì vậy không kết luận “thiếu chấm phẩy luôn sai”, cũng không coi “xuống dòng luôn đủ”. Bài lab dùng chấm phẩy rõ ràng.

Tên phân biệt hoa thường: message và Message là hai tên khác nhau. Chuỗi cần dấu nháy phù hợp. Dùng const khi không gán lại binding và let khi cần gán lại; kiểu dữ liệu và phạm vi sẽ được học kỹ ở bài kế tiếp. Var vẫn tồn tại trong mã cũ, nhưng không cần dùng nó làm mặc định cho mã mới của loạt bài này.

// mở comment đến hết dòng. /* ... */ tạo comment khối, có thể qua nhiều dòng và không lồng theo cách nhiều người mới mong đợi. Comment nên giải thích ý định hoặc giới hạn, chẳng hạn “chỉ dùng dữ liệu DEMO”, thay vì thuật lại từng ký tự. Comment không phải chỗ cất token: nó cũng được gửi xuống browser.

Lab: một module có kết quả quan sát được

Tạo thư mục riêng với index.html và app.js. Dùng HTTP server cục bộ đã chuẩn bị từ bài lab web, chỉ bind loopback và phục vụ thư mục DEMO. Có thể dùng Python 3 nếu đã cài:

python3 -m http.server 8000 --bind 127.0.0.1

Chạy trong đúng thư mục, mở http://127.0.0.1:8000/index.html, dừng server bằng Ctrl+C khi xong. Không chạy từ thư mục chứa dữ liệu cá nhân vì server phục vụ các tệp bên trong nó. Nếu cổng đã dùng, chọn cổng trống và sửa URL. Không cần mở firewall hoặc đưa server lên Internet. Module nên được thử qua HTTP thay vì mở trực tiếp bằng file URL vì quy tắc tải module và origin khác classic script.

<!doctype html>
<html lang="vi">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>DEMO JavaScript module</title>
  <script type="module" src="./app.js"></script>
</head>
<body>
  <h1>Kiểm tra module DEMO</h1>
  <p id="status">HTML đã sẵn nội dung. Chưa xác nhận module.</p>
  <p>Đọc Console để kiểm tra quá trình chạy.</p>
</body>
</html>
/* Dữ liệu giả lập; không đọc tài khoản hay gửi yêu cầu mạng. */
const message = "Module DEMO đã chạy";
let count = 2;
count = count + 1;
console.log("LPI-DEMO", message, count);

const status = document.querySelector("#status");
status.textContent = message + ": " + count;
console.log("DOM-ready", status !== null);

Kỳ vọng đoạn status thành “Module DEMO đã chạy: 3”. Console có dòng LPI-DEMO với thông điệp và số 3, sau đó DOM-ready true. DOM được truy cập ở đây để tạo dấu hiệu quan sát nhỏ; cách xử lý sự kiện và tìm phần tử an toàn sẽ được triển khai đầy đủ ở bài DOM sau. Mã giả định trang có đúng phần tử status như HTML cung cấp.

Mở DevTools từ menu của browser rồi chọn Console. Xác nhận context là trang DEMO đang thử. Nếu Console in undefined sau khi bạn tự gọi console.log, đó có thể là giá trị trả về của lời gọi, không phải lỗi ghi log. Kiểm tra thêm tab Network: app.js phải có phản hồi thành công và nội dung JavaScript đúng. Console là công cụ chẩn đoán, không phải lịch sử kiểm toán tin cậy của server.

Ba lỗi thử có hoàn tác

Đầu tiên đổi src thành missing.js rồi tải lại. Tệp trả 404, status giữ nguyên nội dung HTML, không có hai log thành công từ module. Khôi phục đường dẫn. Nếu chỉ nhìn trang vẫn có chữ và kết luận JavaScript đã chạy, bạn sẽ bỏ qua lỗi tải này.

Tiếp theo thay dòng count bằng cú pháp let count = ; trong bản sao. Tệp vẫn có thể tải HTTP 200 nhưng browser báo SyntaxError và không chạy phần module đó. Khôi phục dòng trước khi thử tiếp. Mã trạng thái HTTP xác nhận việc phân phối tài nguyên, không chứng minh cú pháp đúng.

Cuối cùng đổi #status thành #missing trong script. Query trả null và truy cập textContent gây TypeError. Dòng LPI-DEMO trước đó đã chạy, nhưng dòng DOM-ready phía sau không chạy vì lỗi làm dừng luồng thực thi hiện tại. Khôi phục selector và tải lại: status cùng hai log phải trở về kết quả mong đợi.

Fixture module xác nhận kết quả đúng và hành vi khi có 404, SyntaxError hoặc TypeError.
Hình 2. Fixture module xác nhận kết quả đúng và hành vi khi có 404, SyntaxError hoặc TypeError. Mở hình để xem chi tiết.

Lab được kiểm chứng ngày 23/10/2026 bằng Chromium 151.0.7922.34 với server loopback và context mới cho từng biến thể. Kết quả DOM, log, phạm vi module, 404, SyntaxError và TypeError được đối chiếu bằng assertion. Hai ví dụ ASI/comment được chạy thêm bằng Node 22.22.2. Lệnh Python server cũng được thử trong thư mục tạm chỉ có hai tệp DEMO, kiểm tra byte HTML/JS và dừng bằng SIGINT. Chưa đo hiệu năng hoặc thứ tự thời gian của async/defer, kiểm thử CSP/SRI hay mọi browser.

Phạm vi và tự kiểm tra

Trong Console của trang, window.message không nhận chuỗi chỉ vì module có const message ở mức trên cùng. Đừng dùng phép thử này để kết luận module chưa chạy. Ngược lại, có log cũng chưa chứng minh tính năng xử lý yêu cầu thật đã đúng: lab chưa có form, server nghiệp vụ hoặc lưu trữ dữ liệu.

Hãy phân biệt 404, SyntaxError và TypeError bằng ba lớp tải, phân tích cú pháp và thực thi. Vì sao đặt script trong head với module vẫn đọc được status của ví dụ? Khi nào async làm thứ tự phụ thuộc không đáng tin? Vì sao comment không bảo vệ bí mật? Khi chuyển cùng mã sang Node, phần document nào cần môi trường khác? Câu trả lời là cơ sở để học kiểu dữ liệu, hàm và sự kiện tiếp theo.

Nguồn tham khảo

Bài liên quan trong bộ LPI

Tìm hiểu thêm