Sự Khác Biệt Giữa CommonJS và ES Modules trong JavaScript

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.exports hoặc dùng đối tượng exports như một tham chiếu thuận tiện.
  • Import: Gọi hàm require(path), trả về giá trị của module.exports từ 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 export cho từng thành phần (named export), hoặc export default cho 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ối if, 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/export phả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ị this trong phạm vi toàn cục:undefined, trái ngược với module.exports trong 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" trong package.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àm require trong môi trường ESM, hoặc dùng import() động để tải module CJS dưới dạng promise.

Thẻ: JavaScript es6 commonjs es-modules nodejs

Đăng vào ngày 20 tháng 6 lúc 02:11