本文 AI 產出,尚未審核

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.jsmath/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"

最佳實踐

  1. 每個檔案只做一件事:保持模組單一職責(Single Responsibility),方便測試與重用。
  2. 使用具名匯出作為公共 API,僅在真的需要「唯一入口」時使用 default 匯出。
  3. 聚合模組(barrel)可減少 import 路徑的噪音,但不要過度聚合導致巨大的依賴圖。
  4. 加入 JSDoc 或 TypeScript 宣告檔,讓 IDE 能提供自動完成與型別檢查。
  5. 配合建置工具(Webpack、Rollup、Vite) 設定 output.moduletreeShaking,確保未使用的程式碼會被剔除。

實際應用場景

場景 為何需要模組化 典型實作方式
大型前端單頁應用(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.jslogger.js 開始,逐步導入聚合模組與動態匯入,感受程式碼可讀性與維護成本的明顯改善吧!祝開發順利 🚀