CommonJS và ES Modules (ESM) là hai mô hình tổ chức mã phổ biến nhất trong JavaScript, mỗi cái phục vụ những mục tiêu thiết kế riêng biệt và mang lại các đặc tính vận hành khác nhau. Dưới đây là phân tích kỹ thuật chi tiết, kèm ví dụ minh họa rõ ràng và so sánh cốt lõi.
1. CommonJS: Mô hình đồng bộ cho môi trường máy chủ
Được phát triển chủ yếu cho Node.js, CommonJS dựa trên cơ chế tải đồng bộ và thực thi ngay lập tức khi gọi require().
Cú pháp cơ bản
- Export: Gán giá trị vào
module.exportshoặc dùng đối tượngexportsnhư một tham chiếu thuận tiện. - Import: Gọi hàm
require(path), trả về giá trị củamodule.exportstừ tập tin được chỉ định.
Ví dụ minh họa:
// math-utils.cjs
const PI = Math.PI;
function calculateArea(radius) {
return PI * radius * radius;
}
// Xuất nhiều thành phần qua exports
exports.PI = PI;
exports.area = calculateArea;
// Hoặc ghi đè toàn bộ module.exports
// module.exports = { PI, area: calculateArea };
// app.cjs
const utils = require('./math-utils.cjs');
console.log(utils.PI); // 3.141592653589793
console.log(utils.area(5)); // 78.53981633974483
Đặc điểm nổi bật
- Tải động tại thời điểm chạy:
require()có thể xuất hiện ở bất kỳ đâu — trong hàm, khối điều kiện, thậm chí là trong vòng lặp — với đường dẫn có thể là biểu thức. - Hành vi sao chép giá trị: Với kiểu nguyên thủy (
string,number,boolean), giá trị được sao chép tại thời điểm export. Thay đổi sau đó trong module nguồn không ảnh hưởng đến bản sao đã import. - Tham chiếu đối tượng: Các giá trị dạng object (bao gồm function, array, class) được chia sẻ bằng tham chiếu. Cập nhật thuộc tính bên trong sẽ phản ánh ngay ở nơi import.
- Bộ nhớ đệm tự động: Mỗi module chỉ được thực thi một lần; các lần
require()tiếp theo trả về cùng một instance đã lưu trong bộ nhớ. - Xử lý phụ thuộc vòng: Được hỗ trợ thông qua cơ chế "partial evaluation", nhưng yêu cầu hiểu rõ thứ tự khởi tạo module để tránh lỗi
undefined.
2. ES Modules: Mô hình tĩnh, chuẩn ngôn ngữ
ESM là phần chính thức của ECMAScript 2015, được thiết kế để hoạt động thống nhất trên cả trình duyệt lẫn Node.js, với trọng tâm là khả năng phân tích tĩnh và tối ưu hóa trước khi thực thi.
Cú pháp cơ bản
- Export: Dùng từ khóa
exportcho từng thành phần (named export), hoặcexport defaultcho một giá trị mặc định duy nhất. - Import: Dùng từ khóa
import, bắt buộc nằm ở phạm vi trên cùng của file — không cho phép trong khốiif,for, hay hàm.
Ví dụ minh họa:
// geometry.mjs
export const TAU = 2 * Math.PI;
export function circumference(radius) {
return TAU * radius;
}
export default class Circle {
constructor(r) {
this.radius = r;
}
get area() {
return Math.PI * this.radius ** 2;
}
}
// main.mjs
// Nhập từng thành phần cụ thể
import { TAU, circumference } from './geometry.mjs';
// Nhập lớp mặc định
import Circle from './geometry.mjs';
console.log(TAU); // 6.283185307179586
console.log(circumference(4)); // 25.132741228718345
const c = new Circle(3);
console.log(c.area); // 28.274333882308138
Đặc điểm nổi bật
- Phân tích tĩnh: Tất cả
import/exportphải là hằng số — đường dẫn phải là chuỗi ký tự, không được tính toán. Điều này cho phép công cụ xây dựng xác định chính xác đồ thị phụ thuộc để loại bỏ mã không dùng (tree-shaking). - Liên kết sống (live bindings): Khi một giá trị được export, tất cả nơi import đều giữ liên kết đọc-only tới cùng một vị trí bộ nhớ. Nếu giá trị gốc thay đổi (ví dụ:
export let counter = 0;rồi tăng trong module nguồn), mọi nơi import đều thấy giá trị mới — kể cả kiểu nguyên thủy. - Tải không đồng bộ (trình duyệt): Khi sử dụng thẻ
<script type="module">, trình duyệt tải và xử lý module theo cơ chế non-blocking, tương thích với hệ thống event loop. - Mặc định strict mode: Không cần khai báo
"use strict"; toàn bộ ESM luôn chạy trong chế độ nghiêm ngặt. - Giá trị
thistrong phạm vi toàn cục: Làundefined, trái ngược vớimodule.exportstrong CommonJS. - Xử lý phụ thuộc vòng: Do liên kết sống, việc truy cập giá trị giữa các module tuần hoàn thường an toàn hơn nếu không đọc trước khi module kia hoàn tất khởi tạo.
So sánh tổng quan
| Tính năng | CommonJS | ES Modules |
|---|---|---|
| Mục tiêu thiết kế | Môi trường Node.js, tải đồng bộ | Chuẩn ECMA, hỗ trợ đa nền tảng |
| Thời điểm xác định phụ thuộc | Thời điểm chạy (runtime) | Thời điểm phân tích (parse time) |
| Cú pháp import/export | require(), module.exports |
import, export |
| Vị trí hợp lệ | Bất kỳ đâu trong code | Chỉ ở phạm vi trên cùng |
| Hành vi truyền giá trị | Sao chép (nguyên thủy), tham chiếu (object) | Liên kết sống (đọc-only) cho mọi kiểu |
| Chế độ mặc định | Non-strict | Strict |
Giá trị this toàn cục |
module.exports |
undefined |
| Định dạng file đề xuất | .cjs hoặc .js (kèm "type": "commonjs") |
.mjs hoặc .js (kèm "type": "module") |
Khuyến nghị lựa chọn
- Dự án mới: Ưu tiên ESM — đảm bảo khả năng mở rộng, tối ưu hóa tốt hơn và tuân thủ chuẩn hiện đại.
- Node.js: Sử dụng
"type": "module"trongpackage.jsonđể kích hoạt ESM toàn cục; chuyển đổi dần các thư viện phụ thuộc sang phiên bản ESM-native. - Trình duyệt: Dùng trực tiếp
<script type="module">cho ứng dụng đơn giản; với dự án quy mô lớn, nên kết hợp cùng Vite hoặc esbuild để tận dụng khả năng phân tích tĩnh và hot-module replacement. - Tương thích hỗn hợp: Khi bắt buộc phải kết nối CJS ↔ ESM, nên dùng
createRequire(import.meta.url)để tạo hàmrequiretrong môi trường ESM, hoặc dùngimport()động để tải module CJS dưới dạng promise.