This programming guide is only available in Vietnamese. Switch to Vietnamese to read the full article.
Khi các ứng dụng web ngày càng lớn và phức tạp, việc viết toàn bộ code JavaScript trong một file duy
nhất hoặc nhúng hàng chục thẻ <script> vào trang HTML sẽ dẫn tới thảm họa
Global Scope Pollution (Ô nhiễm phạm vi toàn cầu) và xung đột tên biến. Hệ thống
Module ra đời để cô lập phạm vi (Scope Isolation), tổ chức mã nguồn và tối ưu hóa hiệu năng ứng dụng
thông qua việc phân tích cây phụ thuộc.
1. Tại sao Module hóa lại quan trọng?
Mỗi file JavaScript khi khai báo dạng module sẽ có một Module Scope (Phạm vi mô-đun) hoàn toàn độc lập:
-
Scope Isolation: Các biến, hàm, lớp khai báo trong module đều là biến cục bộ ẩn,
không thể bị đọc hoặc can thiệp từ bên ngoài trừ khi bạn hiển thị chúng ra bằng từ khóa
export. -
Không ô nhiễm toàn cục: Hạn chế hoàn toàn xung đột tên biến toàn cục. Trình duyệt
không cần gắn biến vào đối tượng
window. - Dependency Graph: Dễ dàng quản lý cấu trúc dự án dưới dạng sơ đồ phụ thuộc giúp các công cụ đóng gói (bundlers) hiểu rõ mối liên kết giữa các phần của mã nguồn.
2. Chu kỳ nạp Module 3 pha của ES Modules (V8 Engine)
Khác với CommonJS tải module đồng bộ, ES Modules được thiết kế bất đồng bộ để tối ưu cho môi trường web. V8 Engine (và các JS Engine khác) xử lý ES Modules qua 3 pha tách biệt:
[ Pha 1: Construction ] [ Pha 2: Instantiation ] [ Pha 3: Evaluation ]
Tải file & Phân tích Liên kết các ô nhớ Chạy code & Điền giá trị
(Static Parsing) (Live Bindings) (DFS Post-Order Execution)
main.js main.js main.js (Thực thi cuối)
/ \ / \ / \
lib.js utils.js lib.js utils.js lib.js utils.js
(Khớp các import->export) (Thực thi lib & utils trước)
-
Pha 1: Construction (Xây dựng đồ thị phụ thuộc - Static Parsing): Trình duyệt hoặc
Node.js tìm kiếm, tải về toàn bộ các file JS, phân tích cú pháp tĩnh (Static Parsing) để tạo ra các
Module Record. Trong pha này, engine chỉ quét qua từ khóaimportvàexporttĩnh để xây dựng sơ đồ cây phụ thuộc (Dependency Graph) mà chưa thực thi bất kỳ dòng code nào. Điều này giải thích tại sao câu lệnhimportchuẩn không được phép nằm trong câu lệnh điều kiệnifhay vòng lặp. - Pha 2: Instantiation (Liên kết bộ nhớ - Linking): Engine cấp phát các vùng nhớ (Module Environment Records) cho các biến được xuất/nhập. V8 sẽ ánh xạ các import của module này trỏ trực tiếp vào ô nhớ chứa export của module kia thông qua cơ chế Live Bindings (Liên kết động). Điểm khác biệt tối quan trọng: ESM truyền tham chiếu động, còn CommonJS thì copy giá trị (Copy-on-require).
-
Pha 3: Evaluation (Thực thi mã nguồn): V8 Engine thực thi mã nguồn của từng file
module để điền giá trị cụ thể vào các ô nhớ đã được liên kết ở Pha 2. Quy trình này thực hiện theo
thứ tự duyệt cây DFS (Depth-First Post-Order) từ dưới lên trên (các module lá, độc lập sẽ chạy
trước, module chính
main.jschạy sau cùng). Mỗi module chỉ được evaluate đúng một lần duy nhất và kết quả được lưu vào cache.
3. So sánh chi tiết ES Modules (ESM) và CommonJS (CJS)
Hiểu rõ sự khác biệt giữa hai hệ thống module giúp bạn thiết kế ứng dụng chạy mượt mà trên cả trình duyệt và Node.js:
| Đặc điểm | CommonJS (CJS) | ES Modules (ESM) |
|---|---|---|
| Môi trường chính | Node.js (Server-side) truyền thống | Trình duyệt hiện đại & Node.js mới |
| Thời điểm nạp | Động khi chạy code (Runtime / Synchronous) | Tĩnh trước khi chạy (Static Analysis / Asynchronous) |
| Liên kết xuất/nhập | Copy-by-value / Copy-on-require (Giá trị được sao chép khi require, không cập nhật nếu module nguồn thay đổi biến nguyên thủy) | Live Bindings (Liên kết tham chiếu trực tiếp, cập nhật tự động khi module nguồn đổi giá trị; ở phía nhận là Read-only) |
| Cú pháp import/export | const math = require('./math')module.exports = { add } |
import { add } from './math.js'export { add } |
| Tối ưu Tree Shaking | Hầu như không thể tối ưu tự động do tính chất nạp động của require | Hỗ trợ cực tốt, bundler dễ dàng nhận biết code không dùng |
> [!NOTE] > Live Bindings có ý nghĩa rất lớn. Nếu module counter.js export biến số
let count = 0 và hàm increment(), khi bạn gọi increment(), giá trị biến count ở file import
cũng lập tức tăng lên. Trong CommonJS, biến count đã được copy thành bản sao cục bộ ngay từ lệnh
require, nên nó sẽ mãi là 0 trừ khi bạn bọc nó trong một object tham chiếu.
4. Tối ưu hóa Tree Shaking và Side Effects
Tree Shaking là thuật ngữ mô tả quá trình loại bỏ các đoạn code thừa không bao giờ được gọi (Dead Code Elimination) trong quá trình đóng gói ứng dụng (bundling):
- Cơ chế: Nhờ ES Modules phân tích cấu trúc tĩnh từ Pha 1, bundler (như Vite, Rollup, Webpack) biết chắc chắn hàm nào được import và sử dụng. Những hàm nào được export nhưng không ai dùng sẽ bị loại bỏ khỏi file output cuối cùng.
-
Cấu hình
"sideEffects": Một số module khi import sẽ thực thi code thay đổi môi trường toàn cục (ví dụ thêm polyfill vàoArray.prototypehoặc đăng ký sự kiện window). Hành vi này gọi là Side Effect. Nếu bạn khai báo"sideEffects": falsetrong filepackage.jsoncủa dự án, bạn đang cam đoan với bundler rằng: *"Các module của tôi không có tác dụng phụ ngầm, nếu tôi không import cụ thể cái gì thì hãy cứ thoải mái xóa bỏ nó khỏi bundle."*
5. Named Exports, Default Export và Dynamic Imports
ES Modules hỗ trợ cấu trúc linh hoạt để xuất bản thư viện:
-
Named Exports: Cho phép export nhiều biến/hàm/lớp từ một file. Khi import bắt buộc
phải ghi đúng tên trong ngoặc nhọn:
import { add, subtract } from './math.js'. -
Default Export: Cho phép xuất một thực thể đại diện duy nhất (thường là Class
chính). Khi import không cần dấu ngoặc nhọn và có thể tự đổi tên:
import Database from './db.js'. Trong hậu trường, default export thực chất là một named export với khóa tên làdefault. -
Dynamic Imports (import động): Trong trường hợp bạn muốn tải file JS lười (Lazy
load) chỉ khi cần thiết (ví dụ: người dùng bấm vào nút mở biểu đồ phân tích lớn), hãy sử dụng hàm
import('./chart.js'). Hàm này trả về một Promise chứa module exports, kết hợp độ động của CommonJS và tính năng bất đồng bộ của ESM.
// Minh họa Dynamic Import nạp module bất đồng bộ khi cần
async function loadAnalyticsOnDemand() {
try {
console.log("Đang kích hoạt nạp code phân tích...");
const analytics = await import('./analytics.js');
analytics.trackEvent("UserCheckoutSuccess");
} catch (err) {
console.error("Lỗi nạp mã nguồn động:", err.message);
}
}
Module Scope & Từ Khóa this
Giá trị của từ khóa this ở cấp độ cao nhất (Top-level) trong JavaScript phụ thuộc hoàn
toàn vào chế độ nạp module:
-
Trong ES Modules (ESM): ESM luôn tự động chạy ở chế độ nghiêm ngặt (Strict Mode).
Do đó,
thisở top-level mang giá trịundefined. -
Trong CommonJS (CJS):
thisở top-level của một file Node.js thực chất trỏ tới đối tượngmodule.exports(khởi đầu là một object rỗng{}). -
globalThis: Là đối tượng toàn cục chuẩn hóa (ES2020) giúp bạn truy cập đối tượng
global (như
windowtrên trình duyệt, hoặcglobaltrên Node.js) một cách thống nhất và an toàn mà không cần kiểm tra môi trường.
6. Câu hỏi trắc nghiệm ôn tập
Trắc nghiệm 1: Live Bindings vs Copy-on-require
Phát biểu nào sau đây về cơ chế Live Bindings của ES Modules là ĐÚNG?
7. Playground: Giả Lập Live Bindings vs Copy-on-require
Playground dưới đây mô phỏng sự khác biệt quan trọng giữa ESM Live Bindings và CJS copy-on-require:
8. IIFE Pattern — Lịch Sử Trước Khi Có Modules
Trước khi ES6 Modules ra đời, các lập trình viên dùng IIFE (Immediately Invoked Function Expression) để tạo private scope và tránh ô nhiễm global. Đây là pattern nền tảng của nhiều thư viện cổ điển (jQuery, Lodash):
// IIFE: tạo scope riêng, expose public API
const CounterModule = (function() {
// Private (không thể truy cập từ ngoài)
let _count = 0;
const _history = [];
function _log(action) {
_history.push({ action, count: _count, time: Date.now() });
}
// Public API
return {
increment(by = 1) {
_count += by;
_log('increment');
return this;
},
decrement(by = 1) {
_count -= by;
_log('decrement');
return this;
},
get value() { return _count; },
get history() { return [..._history]; },
reset() { _count = 0; _history.length = 0; return this; }
};
})();
// Dùng như module:
CounterModule.increment().increment(5).decrement(2);
console.log(CounterModule.value); // 5 (1 + 5 - 2 = 4? No: 0 + 1 + 5 - 2 = 4)
console.log(CounterModule.history.length); // 3 actions
// Modern equivalent: ES6 Module
// export const counter = { ... } (không cần IIFE)
// Circular Dependency: ESM handle được, CJS thì không
// a.mjs imports b.mjs, b.mjs imports a.mjs
// ESM: Live Bindings cho phép resolve vì tất cả import bindings đã được cấp phát ở Pha 2
// CJS: circular require thường trả về {} hoặc partial exports → bug!
Trắc nghiệm 2: Dynamic Import
Câu lệnh const mod = await import('./heavy.js') khác gì so với
import { fn } from './heavy.js'?
Tải file code thực hành minh họa bài học
Mã nguồn giả lập Module Scope isolation, so sánh CJS copy-on-require vs ESM Live Bindings, IIFE pattern và Dynamic Import lazy-loading:
Tải về module_system.js
Comments
Bình luận