JavaScript ES6+ 新特性:模組化(import / export)
簡介
在過去的 JavaScript 開發環境中,所有程式碼往往都寫在同一個 <script> 標籤或是全域變數裡,隨著專案規模的擴大,維護成本與命名衝突的問題日益顯著。ES6(ECMAScript 2015)正式引入 模組(module) 機制,讓開發者可以把功能切分成獨立檔案,透過 export 暴露介面、import 引入使用。模組化不僅提升程式碼的可讀性、可測試性,也讓 tree‑shaking、懶載入(lazy loading)等現代前端建置工具得以發揮效能。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,逐步帶你掌握 ES6+ 模組化的核心,並提供實務上常見的應用情境,幫助你在專案中快速上手、寫出更乾淨、可維護的程式碼。
核心概念
1. 模組的基本概念
- 模組是一個獨立的檔案,內部的變數、函式、類別預設是私有的,只有透過
export明確宣告的成員才會被外部存取。 import則是從其他模組取得這些公開的成員。- 瀏覽器原生支援 ESM(ECMAScript Module),只要在
<script>標籤加上type="module",或在 Node.js 中使用.mjs副檔名(或在package.json設定"type": "module"),即可使用import/export。
2. 匯出(export)方式
| 匯出方式 | 語法 | 說明 |
|---|---|---|
| 具名匯出(named export) | export const foo = ...;export function bar() {} |
可以匯出多個成員,匯入時必須使用相同名稱(或使用別名)。 |
| 預設匯出(default export) | export default function () {}export default class MyClass {} |
每個模組只能有 一個 預設匯出,匯入時可自行命名。 |
| 匯出列表(export list) | const a = 1; const b = 2; export { a, b as aliasB }; |
先宣告再一次性匯出,適合集中管理。 |
3. 匯入(import)方式
| 匯入方式 | 語法 | 說明 |
|---|---|---|
| 具名匯入 | import { foo, bar } from './module.js'; |
必須使用大括號,名稱必須與匯出相符。 |
| 預設匯入 | import MyDefault from './module.js'; |
可以自行命名,對應匯出的 default 成員。 |
| 混合匯入 | import MyDefault, { foo, bar } from './module.js'; |
同時取得 default 與具名成員。 |
| 匯入全部 | import * as utils from './utils.js'; |
把模組的所有具名匯出放入一個命名空間(object)中。 |
| 重新匯出 | export { foo } from './module.js'; |
直接把其他模組的成員再匯出,常用於建立「聚合模組」。 |
4. 動態匯入(dynamic import)
使用 import() 函式可以在程式執行時才載入模組,回傳 Promise,非常適合實作 懶載入、代碼分割(code splitting)。
// 只有在使用者點擊按鈕時才載入 heavyModule
document.getElementById('loadBtn').addEventListener('click', async () => {
const { heavyFunction } = await import('./heavyModule.js');
heavyFunction();
});
程式碼範例
範例 1:最簡單的具名匯出與匯入
utils.js(具名匯出)
// utils.js
export function add(a, b) {
return a + b;
}
export const PI = 3.14159;
main.js(具名匯入)
// main.js
import { add, PI } from './utils.js';
console.log('2 + 3 =', add(2, 3)); // 2 + 3 = 5
console.log('圓周率 =', PI); // 圓周率 = 3.14159
重點:具名匯入必須使用大括號,且名稱必須與匯出保持一致。
範例 2:預設匯出與混合匯入
logger.js(預設匯出)
// logger.js
export default class Logger {
constructor(prefix = '') {
this.prefix = prefix;
}
log(message) {
console.log(`[${this.prefix}] ${message}`);
}
}
// 也可以同時具名匯出其他工具
export function formatDate(date) {
return date.toISOString().split('T')[0];
}
app.js(混合匯入)
// app.js
import Logger, { formatDate } from './logger.js';
const logger = new Logger('MyApp');
logger.log('啟動應用程式'); // [MyApp] 啟動應用程式
console.log('今天日期:', formatDate(new Date()));
技巧:預設匯出允許在匯入時自行命名,讓類別或函式的意圖更明確。
範例 3:匯出列表與別名(alias)
constants.js
// constants.js
const API_URL = 'https://api.example.com';
const TIMEOUT = 5000;
export { API_URL as BASE_URL, TIMEOUT };
service.js
// service.js
import { BASE_URL, TIMEOUT } from './constants.js';
export async function fetchData(endpoint) {
const response = await fetch(`${BASE_URL}/${endpoint}`, { timeout: TIMEOUT });
return response.json();
}
說明:使用
as為匯出成員取別名,可在不同檔案間保持語意一致。
範例 4:聚合模組(barrel)與重新匯出
math/add.js、math/subtract.js(各自具名匯出)
// math/add.js
export function add(a, b) { return a + b; }
// math/subtract.js
export function subtract(a, b) { return a - b; }
math/index.js(聚合)
// math/index.js
export { add } from './add.js';
export { subtract } from './subtract.js';
app.js(一次匯入所有)
// app.js
import * as math from './math/index.js';
console.log(math.add(10, 5)); // 15
console.log(math.subtract(10, 5)); // 5
好處:聚合模組讓外部只需要關注一個入口檔案,減少 import 路徑的管理成本。
範例 5:動態匯入與錯誤處理
// lazyLoadChart.js
export async function loadChart() {
try {
const { Chart } = await import('chart.js'); // 第三方套件
const ctx = document.getElementById('myChart').getContext('2d');
new Chart(ctx, { type: 'bar', data: {/* ... */} });
} catch (err) {
console.error('載入 Chart.js 失敗:', err);
}
}
// 使用者點擊後才載入
document.getElementById('showChart').addEventListener('click', () => {
loadChart();
});
實務:動態匯入非常適合 SPA 中的路由懶載入、或是只在特定條件下才需要的功能(如圖表、PDF 產生器)。
常見陷阱與最佳實踐
| 常見問題 | 可能原因 | 解決方法 / 最佳實踐 |
|---|---|---|
| 1. 重複匯入同一模組 | 多個檔案分別 import 同一模組,導致執行時產生多次副作用(如初始化程式碼) |
模組只執行一次,但為避免副作用,將初始化邏輯放在函式內或使用 singleton。 |
| 2. 預設匯出與具名匯出混亂 | 同時使用 export default 與同名具名匯出,匯入時產生命名衝突 |
保持一致性:若主要匯出是一個類別或函式,使用 default;若是工具集合,使用具名匯出。 |
| 3. 循環依賴(circular dependency) | A 模組匯入 B,B 又匯入 A,導致未定義或 undefined 錯誤 | 重新設計模組邊界,或將共同依賴抽到第三個模組;必要時使用 動態匯入 延遲載入。 |
| 4. 路徑大小寫問題 | 在 macOS(不區分大小寫)開發,部署到 Linux(區分大小寫)時找不到檔案 | 統一使用小寫或 kebab-case,並在 IDE 中啟用路徑檢查。 |
5. 忘記在 <script> 加 type="module" |
瀏覽器直接執行 ES6 模組語法會拋錯 | 在 HTML 中加入 <script type="module" src="main.js"></script>,或在 Node.js 中使用 .mjs / "type":"module"。 |
最佳實踐
- 每個檔案只做一件事:保持模組單一職責(Single Responsibility),方便測試與重用。
- 使用具名匯出作為公共 API,僅在真的需要「唯一入口」時使用 default 匯出。
- 聚合模組(barrel)可減少 import 路徑的噪音,但不要過度聚合導致巨大的依賴圖。
- 加入 JSDoc 或 TypeScript 宣告檔,讓 IDE 能提供自動完成與型別檢查。
- 配合建置工具(Webpack、Rollup、Vite) 設定
output.module、treeShaking,確保未使用的程式碼會被剔除。
實際應用場景
| 場景 | 為何需要模組化 | 典型實作方式 |
|---|---|---|
| 大型前端單頁應用(SPA) | 功能分割、路由懶載入、團隊協作 | 每個頁面或功能區塊建立獨立模組,使用 import() 於路由切換時載入。 |
| 共用函式庫(Utility Library) | 多專案共享、避免重複程式碼 | 建立 utils/ 目錄,使用具名匯出,透過聚合模組提供單一入口 index.js。 |
| Node.js 後端服務 | 依賴注入、測試友好 | 使用 ES 模組 (.mjs) 或在 package.json 設 "type":"module",把路由、控制器、服務層分別模組化。 |
| 第三方套件開發 | 讓使用者只需 import { X } from 'my-lib' |
在 src/ 中分割功能,最後在 src/index.js 重新匯出所有公開 API。 |
| SSR(Server‑Side Rendering)與 CSR(Client‑Side Rendering)共用程式碼 | 同一套程式碼在兩端執行,需要明確的模組界限 | 把純邏輯抽成獨立模組,分別在 server/ 與 client/ 中 import。 |
總結
ES6+ 的 模組化 為 JavaScript 帶來了前所未有的結構化與可維護性。透過 export/import,我們可以:
- 明確劃分功能,讓每個檔案只關注單一職責。
- 避免全域汙染,減少命名衝突與不必要的副作用。
- 支援懶載入、code‑splitting,提升應用效能。
- 配合現代建置工具,自動剔除未使用的程式碼(tree‑shaking)。
在實務開發中,建議先從 具名匯出 作為公共 API,僅在需要唯一入口時使用 預設匯出。善用 聚合模組 減少 import 雜訊,同時注意 循環依賴、路徑大小寫 等常見陷阱。結合動態匯入與現代前端框架(React、Vue、Svelte),即可打造出 模組化、可伸縮、易測試 的現代 Web 應用。
開始行動:把現有的巨型
app.js拆成多個小模組,從最簡單的utils.js、logger.js開始,逐步導入聚合模組與動態匯入,感受程式碼可讀性與維護成本的明顯改善吧!祝開發順利 🚀