本文 AI 產出,尚未審核

JavaScript 模組與封裝:匯入 / 匯出語法完整指南


簡介

在現代前端與 Node.js 開發中,模組化已成為不可或缺的基礎概念。它讓我們可以把程式切割成小而獨立的單位,提升 可維護性、可測試性,同時避免全域變數的汙染。ES6(ECMAScript 2015)正式引入的 import / export 語法,取代了過去的 CommonJS、AMD 等模組系統,成為現在最主流的模組寫法。

本篇文章將從 概念、語法、實作範例 逐步說明,並針對常見的陷阱與最佳實踐提供建議,讓初學者到中階開發者都能在實務上快速上手、正確運用模組與封裝。


核心概念

1. 為什麼需要模組?

  • 避免命名衝突:每個模組都有自己的執行環境(module scope),不會直接污染全域。
  • 程式碼重用:一次寫好、到處引用,降低重複開發成本。
  • 清晰的依賴關係:透過 import 明確宣告需要的功能,讓檔案之間的關係一目了然。

2. ES6 模組的兩大關鍵字:exportimport

關鍵字 用途 常見變形
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 只暴露 Esquareapp.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 模組 在同一專案中同時使用 requiremodule.exportsimport/export 會造成編譯或執行時錯誤。 盡量統一使用 ES6 模組;若必須相容舊有套件,使用 importcreateRequire(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),或手動移除未使用的匯入。

最佳實踐小結

  1. 盡量使用命名匯出,除非模組只有單一功能才使用預設匯出。
  2. 保持匯入路徑簡潔且明確(使用 @/~ 等別名可提升可讀性)。
  3. 封裝私有變數:利用模組作用域避免全域污染。
  4. 使用動態匯入實作懶載入,提升首屏載入速度。
  5. 設定 lint 規則(如 eslint-plugin-import)避免未使用或錯誤的匯入。

實際應用場景

場景 需求 典型模組設計
大型單頁應用(SPA) 多頁面共用 UI 元件、狀態管理、路由等 每個功能區塊(components、store、router)各自建立模組,使用聚合入口 index.js 統一匯出。
Node.js 後端服務 資料庫操作、業務邏輯、API 路由分離 每個路由檔案 export default router,在 app.jsimport userRouter from './routes/user.js'
微前端(Micro‑frontend) 多個獨立子應用在同一頁面共存 子應用以 預設匯出 暴露初始化函式,父應用使用 import() 動態載入子應用。
第三方套件開發 提供公共函式庫或 UI 元件庫 使用 命名匯出 讓使用者自行挑選需要的功能,並在 package.json 設定 module 指向 ES6 入口。
測試環境 需要 mock 某些依賴 利用 jest.mock() 搭配 ES6 模組的靜態結構,輕鬆替換匯入的實作。

總結

  • 模組化是現代 JavaScript 開發的基礎,export / import 提供了清晰、靜態的依賴描述方式。
  • 命名匯出適合多功能模組,預設匯出則適合單一入口的情境。
  • 透過 聚合匯出動態匯入封裝私有變數,我們可以同時兼顧程式碼可讀性、效能與安全性。
  • 避免常見陷阱(混用模組系統、循環依賴、未使用匯入),並遵循最佳實踐(使用 lint、Tree‑shaking、懶載入)即可寫出 可維護、可擴充 的程式碼基礎。

掌握了匯入 / 匯出的語法與概念,你就能在任何規模的 JavaScript 專案中,建立乾淨、模組化的程式結構,為日後的功能擴充與團隊協作奠定堅實基礎。祝開發順利,程式碼永遠保持 乾淨可讀