JavaScript 模組與封裝:匯入 / 匯出語法完整指南
簡介
在現代前端與 Node.js 開發中,模組化已成為不可或缺的基礎概念。它讓我們可以把程式切割成小而獨立的單位,提升 可維護性、可測試性,同時避免全域變數的汙染。ES6(ECMAScript 2015)正式引入的 import / export 語法,取代了過去的 CommonJS、AMD 等模組系統,成為現在最主流的模組寫法。
本篇文章將從 概念、語法、實作範例 逐步說明,並針對常見的陷阱與最佳實踐提供建議,讓初學者到中階開發者都能在實務上快速上手、正確運用模組與封裝。
核心概念
1. 為什麼需要模組?
- 避免命名衝突:每個模組都有自己的執行環境(module scope),不會直接污染全域。
- 程式碼重用:一次寫好、到處引用,降低重複開發成本。
- 清晰的依賴關係:透過
import明確宣告需要的功能,讓檔案之間的關係一目了然。
2. ES6 模組的兩大關鍵字:export 與 import
| 關鍵字 | 用途 | 常見變形 |
|---|---|---|
export |
把變數、函式、類別等 公開 給其他模組使用 | export const, export function, export default |
import |
從其他模組 匯入 已公開的成員 | import {}, import * as, import defaultName |
注意:ES6 模組是 靜態 的,也就是說在編譯階段就能確定匯入與匯出的名稱,這使得工具如 Tree‑shaking(去除未使用代碼)成為可能。
3. export 的三種寫法
3.1 命名匯出(Named Export)
// utils.js
export const PI = 3.14159;
export function sum(a, b) {
return a + b;
}
export class Calculator {
multiply(x, y) {
return x * y;
}
}
- 優點:匯入時可以只挑選需要的成員,減少記憶體佔用。
- 使用方式:
import { PI, sum } from './utils.js';
console.log(PI); // 3.14159
console.log(sum(2, 3)); // 5
3.2 預設匯出(Default Export)
// logger.js
export default function log(message) {
console.log(`[LOG] ${message}`);
}
- 優點:每個模組只能有一個預設匯出,匯入時可以自行命名。
- 使用方式:
import log from './logger.js'; // 自訂名稱 log
log('模組已載入');
3.3 匯出聚合(Export Aggregation)
在大型專案中,常會把多個子模組集中在一個「入口」檔案:
// index.js
export * from './utils.js';
export { default as log } from './logger.js';
這樣其他檔案只要 import index.js 即可一次取得所有功能。
4. import 的變形
| 形式 | 範例 | 說明 |
|---|---|---|
| 完整匯入 | import * as utils from './utils.js'; |
把所有命名匯出掛在 utils 物件下 |
| 部分匯入 | import { sum, PI } from './utils.js'; |
僅挑選需要的成員 |
| 預設匯入 | import log from './logger.js'; |
匯入預設匯出,可自行命名 |
| 重新命名 | import { sum as add } from './utils.js'; |
匯入後改名,避免衝突 |
| 混合匯入 | import log, { sum, PI } from './utils.js'; |
同時匯入預設與命名匯出 |
程式碼範例
以下提供 5 個實務中常見的範例,從最簡單到較進階的使用情境。
範例 1:基本的命名匯出與匯入
// math.js
export const E = 2.71828;
export function square(x) {
return x * x;
}
// app.js
import { E, square } from './math.js';
console.log('e 的值:', E);
console.log('5 的平方:', square(5));
說明:
math.js只暴露E與square,app.js直接使用,保持了清晰的依賴關係。
範例 2:預設匯出搭配命名匯出
// apiClient.js
export default class ApiClient {
constructor(baseURL) {
this.baseURL = baseURL;
}
get(path) {
return fetch(`${this.baseURL}${path}`).then(r => r.json());
}
}
export const TIMEOUT = 5000;
// main.js
import ApiClient, { TIMEOUT } from './apiClient.js';
const client = new ApiClient('https://api.example.com/');
client.get('/users').then(console.log);
console.log('請求超時設定:', TIMEOUT);
說明:同一檔案同時有預設類別與命名常數,匯入時可以混合使用。
範例 3:聚合匯出(Barrel)與樹搖(Tree‑shaking)
// services/userService.js
export function fetchUser(id) {
return fetch(`/api/users/${id}`).then(r => r.json());
}
// services/postService.js
export function fetchPost(id) {
return fetch(`/api/posts/${id}`).then(r => r.json());
}
// services/index.js
export * from './userService.js';
export * from './postService.js';
// dashboard.js
import { fetchUser } from './services/index.js'; // 只匯入需要的函式
fetchUser(1).then(console.log);
說明:使用聚合檔案
services/index.js作為入口,Webpack/Rollup 能夠在編譯時剔除未使用的fetchPost,減少最終 bundle 大小。
範例 4:動態匯入(Dynamic Import)與懶載入
// heavyModule.js
export function heavyCalculation() {
// 假裝這裡有大量運算
return '計算完成';
}
// main.js
document.getElementById('runBtn').addEventListener('click', async () => {
const { heavyCalculation } = await import('./heavyModule.js');
console.log(heavyCalculation());
});
說明:
import()會回傳一個 Promise,只有在使用者點擊按鈕時才載入heavyModule.js,有效降低初始載入時間。
範例 5:模組封裝(Encapsulation)與私有變數
// counter.js
let count = 0; // 私有變數,不會被外部直接存取
function increase() { ++count; }
function decrease() { --count; }
function getCount() { return count; }
export default {
increase,
decrease,
getCount,
};
// app.js
import counter from './counter.js';
counter.increase();
counter.increase();
console.log('目前計數:', counter.getCount()); // 2
// console.log(counter.count); // undefined,外部無法直接讀取
說明:透過模組作用域,
count成為私有變數,只能透過公開的 API 操作,實現 資訊隱蔽(Encapsulation)。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 / 最佳實踐 |
|---|---|---|
| 混用 CommonJS 與 ES6 模組 | 在同一專案中同時使用 require、module.exports 與 import/export 會造成編譯或執行時錯誤。 |
盡量統一使用 ES6 模組;若必須相容舊有套件,使用 import 的 createRequire(Node >12)或在 package.json 設定 "type": "module"。 |
| 預設匯出與命名匯出同名 | 同時有 export default function foo(){} 與 export function foo(){} 會產生名稱衝突。 |
避免重名,或改用 export { foo as default } 明確指定。 |
忘記加 .js 副檔名(在瀏覽器原生模組) |
import './module' 會因找不到檔案而拋錯。 |
必須寫完整路徑(含副檔名),或使用 bundler 處理別名。 |
| 循環依賴(Circular Dependency) | A 模組匯入 B,B 又匯入 A,可能導致未定義或空值。 | 重新設計模組邊界,或將共用部分抽離成第三個模組。 |
| 大量一次性匯入 | import * as lib from './bigLib.js' 會一次載入所有程式碼,浪費資源。 |
使用 部分匯入 或 動態匯入,只在需要時才載入。 |
| 未使用的匯入 | 編譯後仍保留未使用的代碼,增加 bundle 體積。 | 開啟 Tree‑shaking(Webpack/Rollup),或手動移除未使用的匯入。 |
最佳實踐小結:
- 盡量使用命名匯出,除非模組只有單一功能才使用預設匯出。
- 保持匯入路徑簡潔且明確(使用
@/、~等別名可提升可讀性)。 - 封裝私有變數:利用模組作用域避免全域污染。
- 使用動態匯入實作懶載入,提升首屏載入速度。
- 設定 lint 規則(如
eslint-plugin-import)避免未使用或錯誤的匯入。
實際應用場景
| 場景 | 需求 | 典型模組設計 |
|---|---|---|
| 大型單頁應用(SPA) | 多頁面共用 UI 元件、狀態管理、路由等 | 每個功能區塊(components、store、router)各自建立模組,使用聚合入口 index.js 統一匯出。 |
| Node.js 後端服務 | 資料庫操作、業務邏輯、API 路由分離 | 每個路由檔案 export default router,在 app.js 中 import userRouter from './routes/user.js'。 |
| 微前端(Micro‑frontend) | 多個獨立子應用在同一頁面共存 | 子應用以 預設匯出 暴露初始化函式,父應用使用 import() 動態載入子應用。 |
| 第三方套件開發 | 提供公共函式庫或 UI 元件庫 | 使用 命名匯出 讓使用者自行挑選需要的功能,並在 package.json 設定 module 指向 ES6 入口。 |
| 測試環境 | 需要 mock 某些依賴 | 利用 jest.mock() 搭配 ES6 模組的靜態結構,輕鬆替換匯入的實作。 |
總結
- 模組化是現代 JavaScript 開發的基礎,
export/import提供了清晰、靜態的依賴描述方式。 - 命名匯出適合多功能模組,預設匯出則適合單一入口的情境。
- 透過 聚合匯出、動態匯入與 封裝私有變數,我們可以同時兼顧程式碼可讀性、效能與安全性。
- 避免常見陷阱(混用模組系統、循環依賴、未使用匯入),並遵循最佳實踐(使用 lint、Tree‑shaking、懶載入)即可寫出 可維護、可擴充 的程式碼基礎。
掌握了匯入 / 匯出的語法與概念,你就能在任何規模的 JavaScript 專案中,建立乾淨、模組化的程式結構,為日後的功能擴充與團隊協作奠定堅實基礎。祝開發順利,程式碼永遠保持 乾淨 與 可讀!