JavaScript ES Modules: import/export — Hướng Dẫn Toàn Diện Từ Cơ Bản Đến Nâng Cao
Khám phá ES Modules (ECMAScript Modules) — hệ thống module chuẩn của JavaScript. Từ cú pháp import/export, default vs named exports, dynamic import, tree shaking đến các best practices cho dự án thực tế.

Giới Thiệu Về ES Modules
Nếu bạn đã từng viết JavaScript trong một dự án thực tế, chắc chắn bạn đã gặp những dòng code như import React from 'react' hay export default function App(). Đó chính là ES Modules (ECMAScript Modules) — hệ thống module chuẩn của JavaScript, được giới thiệu từ ES6 (ES2015).
Trước khi ES Modules ra đời, JavaScript không có cơ chế module chính thức. Các nhà phát triển phải dùng:
- Script tags riêng lẻ — dễ gây xung đột biến toàn cục
- IIFE (Immediately Invoked Function Expression) — gây rối và khó bảo trì
- CommonJS (require) — chủ yếu dùng trong Node.js, không hoạt động trên trình duyệt
- AMD (Asynchronous Module Definition) — phức tạp, ít dùng
ES Modules ra đời để giải quyết tất cả những vấn đề đó, mang đến một hệ thống module thống nhất cho cả trình duyệt lẫn server.
Tại Sao Cần Module?
Trước hết, hãy hiểu tại sao module lại quan trọng:
// ❌ Không có module — mọi thứ trong global scope
// file1.js
var count = 0;
function increment() { count++; }
// file2.js — vô tình ghi đè!
var count = 'hello'; // Mất dữ liệu từ file1.js!
Với module, mỗi file có scope riêng:
// ✅ Có module — mỗi file có scope riêng
// counter.js
let count = 0;
export function increment() { count++; }
export function getCount() { return count; }
// app.js
import { increment, getCount } from './counter.js';
increment();
console.log(getCount()); // 1
Lợi ích của module:
- Encapsulation: Mỗi module có scope riêng, không xung đột
- Reusability: Dễ dàng tái sử dụng code giữa các file
- Maintainability: Code được tổ chức rõ ràng, dễ bảo trì
- Dependency management: Quản lý phụ thuộc rõ ràng
- Tree shaking: Loại bỏ code không dùng
Cú Pháp Cơ Bản: Export
Named Export
Named export cho phép export nhiều giá trị từ một module:
// utils.js
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
export class Calculator {
constructor(name) {
this.name = name;
}
describe() {
return `Calculator: ${this.name}`;
}
}
Hoặc export ở cuối file:
// utils.js
const PI = 3.14159;
function add(a, b) { return a + b; }
function multiply(a, b) { return a * b; }
export { PI, add, multiply };
Và đặt alias (bí danh) khi export:
const PI = 3.14159;
function add(a, b) { return a + b; }
export { PI as pi, add as sum };
Default Export
Mỗi module chỉ có một default export:
// math.js
export default function add(a, b) {
return a + b;
}
Với class:
// Button.js
export default class Button {
constructor(label) {
this.label = label;
}
render() {
return `<button>${this.label}</button>`;
}
}
Với object:
// config.js
export default {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3
};
Kết Hợp Named Export và Default Export
// components.js
export default function Button(props) {
return `<button>${props.label}</button>`;
}
export function Input(props) {
return `<input type="${props.type || 'text'}" />`;
}
export function Select(props) {
return `<select>${props.options.map(o => `<option>${o}</option>`).join('')}</select>`;
}
Cú Pháp Cơ Bản: Import
Import Named Export
// app.js
import { add, multiply, PI } from './utils.js';
console.log(add(2, 3)); // 5
console.log(multiply(4, 5)); // 20
console.log(PI); // 3.14159
Import với alias:
import { add as sum, multiply as product } from './utils.js';
console.log(sum(2, 3)); // 5
console.log(product(4, 5)); // 20
Import tất cả với namespace:
import * as Utils from './utils.js';
console.log(Utils.add(2, 3)); // 5
console.log(Utils.PI); // 3.14159
console.log(Utils.multiply(4, 5)); // 20
Import Default Export
// app.js
import add from './math.js'; // Có thể đặt tên tùy ý
console.log(add(10, 5)); // 15
// Hoặc tên khác
import sum from './math.js';
console.log(sum(10, 5)); // 15
Import Cả Default và Named
import Button, { Input, Select } from './components.js';
Button({ label: 'Click me' }); // Default export
Input({ type: 'text' }); // Named export
Re-export (Tái Xuất)
Re-export cho phép một module đóng vai trò trung gian, tập hợp các export từ nhiều module khác:
// index.js — barrel export
export { default as Button } from './Button.js';
export { default as Input } from './Input.js';
export { default as Select } from './Select.js';
export { default as Modal } from './Modal.js';
Khi đó, các file khác chỉ cần import từ một nơi:
// App.js
import { Button, Input, Select } from './components/index.js';
Re-export với đổi tên:
export { add as sum } from './math.js';
export { default as MathUtils } from './math.js';
Static Import vs Dynamic Import
Static Import (import declaration)
Đây là dạng import bạn thấy hàng ngày — phải đặt ở đầu file, không thể thay đổi trong runtime:
import { debounce } from 'lodash-es';
import React from 'react';
Đặc điểm:
- Phân tích tại compile-time
- Tự động hoisted (đưa lên đầu)
- Luôn load trước khi code chạy
- Hỗ trợ tree shaking
Dynamic Import (import())
Dynamic import là một function trả về Promise, cho phép load module theo điều kiện:
// Load module khi cần
button.addEventListener('click', async () => {
const { showToast } = await import('./toast.js');
showToast('Hello from dynamic import!');
});
// Load theo điều kiện
if (user.isAdmin) {
const { AdminPanel } = await import('./admin/AdminPanel.js');
adminPanel.render();
}
// Load với đường dẫn động
const moduleName = getUserLanguage();
const { greet } = await import(`./locales/${moduleName}.js`);
console.log(greet());
Đặc điểm:
- Phân tích tại runtime
- Trả về Promise (có thể dùng await)
- Không hỗ trợ tree shaking
- Tuyệt vời cho code splitting và lazy loading
Script Type Module
Trên trình duyệt, sử dụng thẻ <script type="module">:
<!-- index.html -->
<script type="module" src="/js/app.js"></script>
Đặc điểm của <script type="module">:
<!-- 1. Tự động defer (load sau khi HTML parsed) -->
<script type="module" src="app.js"></script>
<!-- 2. Có scope riêng — không làm ô nhiễm global -->
<script type="module">
const x = 42; // Không phải global
console.log(x); // 42
</script>
<script>
console.log(typeof x); // undefined
</script>
<!-- 3. Chỉ chạy một lần dù được import nhiều lần -->
<script type="module" src="app.js"></script>
<script type="module" src="app.js"></script>
<!-- app.js chỉ chạy 1 lần! -->
<!-- 4. CORS — phải serve từ server, không dùng file:// -->
Inline module:
<script type="module">
import { showMessage } from './utils.js';
showMessage('Hello from inline module!');
</script>
ES Modules với Node.js
Node.js hỗ trợ ES Modules từ v14+ ổn định. Có hai cách:
Cách 1: “type”: “module” trong package.json
{
"name": "my-project",
"type": "module",
"scripts": {
"start": "node src/index.js"
}
}
Khi đó, tất cả file .js đều dùng ESM:
// index.js
import { readFile } from 'fs/promises';
import express from 'express';
Cách 2: Đuôi .mjs
File .mjs luôn được xử lý là ESM, không cần cấu hình:
// app.mjs
import { createServer } from 'http';
const server = createServer((req, res) => {
res.end('Hello ES Modules!');
});
server.listen(3000);
Lưu ý khi dùng ESM trong Node.js
// ❌ Không có __dirname, __filename, require
// console.log(__dirname); // ReferenceError!
// ✅ Dùng import.meta.url thay thế
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// ✅ require() không dùng được, phải dùng:
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const pkg = require('./package.json');
Tree Shaking
Tree shaking là một trong những lợi ích lớn nhất của ES Modules. Vì ESM là static, bundler biết chính xác export nào được dùng và export nào không.
// math.js
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
export function multiply(a, b) { return a * b; }
export function divide(a, b) { return a / b; }
// app.js
import { add, multiply } from './math.js';
console.log(add(2, 3));
console.log(multiply(4, 5));
Với tree shaking, subtract và divide sẽ bị loại bỏ khỏi bundle cuối cùng!
Điều kiện để tree shaking hoạt động:
- Chỉ dùng ES Modules (import/export), không dùng CommonJS
- Side-effect-free — module không có tác dụng phụ
- Bundler hỗ trợ (Webpack, Vite, Rollup, esbuild)
// package.json — đánh dấu module không có side effects
{
"sideEffects": false
}
ES Modules vs CommonJS
| Tính năng | ES Modules (import/export) | CommonJS (require/module.exports) |
|---|---|---|
| Cú pháp | import/export | require()/module.exports |
| Thời điểm | Static (compile-time) | Dynamic (runtime) |
| Load | Bất đồng bộ (trình duyệt) | Đồng bộ (Node.js) |
| Tree shaking | ✅ Có | ❌ Không |
| Live binding | ✅ Có (tham chiếu sống) | ❌ Sao chép giá trị |
| Top-level await | ✅ Có | ❌ Không |
| Trình duyệt | ✅ Native | ❌ Cần bundler |
| Node.js | ✅ (v14+) | ✅ Mặc định |
Ví dụ về live binding:
// counter.js
export let count = 0;
export function increment() {
count++;
}
// app.js
import { count, increment } from './counter.js';
console.log(count); // 0
increment();
console.log(count); // 1 ✅ Live binding — giá trị cập nhật!
Với CommonJS:
// counter.js
let count = 0;
module.exports = { count, increment() { count++; } };
// app.js
const { count, increment } = require('./counter.js');
console.log(count); // 0
increment();
console.log(count); // 0 ❌ Sao chép giá trị, không thay đổi!
Best Practices
1. Ưu tiên Named Export
// ✅ Tốt — named export
export function formatDate(date) { ... }
export function parseDate(str) { ... }
// ❌ Tránh — default export cho function
export default function formatDate(date) { ... }
Lý do: named export cho auto-complete tốt hơn, dễ tái cấu trúc, và tree shaking hiệu quả hơn.
2. Sử Dụng Barrel Export
// components/index.js
export { Button } from './Button.js';
export { Input } from './Input.js';
export { Modal } from './Modal.js';
3. Tổ Chức Import Rõ Ràng
// 1. Third-party imports
import React, { useState, useEffect } from 'react';
import { format } from 'date-fns';
// 2. Internal absolute imports
import { Button } from '@/components';
import { useAuth } from '@/hooks';
// 3. Relative imports
import { formatDate } from './utils';
import styles from './Button.module.css';
4. Dynamic Import Cho Code Splitting
// React + Vite
const HeavyComponent = React.lazy(() => import('./HeavyComponent.jsx'));
function App() {
return (
<Suspense fallback={<Loading />}>
<HeavyComponent />
</Suspense>
);
}
5. Tránh Import Quá Nhiều
// ❌ Import cả thư viện
import lodash from 'lodash';
lodash.debounce(fn, 300);
// ✅ Import chỉ những gì cần
import debounce from 'lodash/debounce';
debounce(fn, 300);
6. Dùng Đuôi File Đầy Đủ
// ✅ Luôn ghi đuôi file .js (hoặc .mjs)
import { add } from './math.js';
// ❌ Thiếu đuôi — không hoạt động trên trình duyệt
import { add } from './math';
Kết Luận
ES Modules đã thay đổi hoàn toàn cách chúng ta tổ chức code JavaScript. Từ cú pháp import/export đơn giản, tree shaking, dynamic import, đến live binding — tất cả tạo nên một hệ thống module mạnh mẽ, hiệu quả.
Tóm tắt những điểm chính:
- Named export cho nhiều giá trị, default export cho một giá trị chính
- Static import ở đầu file, dynamic import cho code splitting
- Tree shaking giúp bundle nhỏ hơn nhờ loại bỏ code không dùng
- Live binding cho phép tham chiếu sống thay vì sao chép giá trị
- Dùng
"type": "module"trong package.json cho Node.js <script type="module">trên trình duyệt
Hãy bắt đầu áp dụng ES Modules trong dự án của bạn ngay hôm nay — dù bạn viết React, Vue, hay vanilla JavaScript, đây là kỹ năng không thể thiếu của một web developer hiện đại.
Bài viết thuộc series JavaScript nâng cao của Devs2.org. Hãy để lại bình luận nếu bạn có thắc mắc!