JavaScript Modules (模組)

ES6 模組 (ES Modules, ESM) 是 JavaScript 官方的模組系統,讓你可以將程式碼分割成多個檔案,並在檔案之間 import(匯入)和 export(匯出)功能。

為什麼需要模組?

在沒有模組系統之前,所有 JavaScript 程式碼都在全域作用域 (global scope) 中執行,容易造成命名衝突和維護困難。模組系統解決了這些問題:

  • 避免命名衝突:每個模組有自己的作用域
  • 程式碼重用:可以輕鬆在不同專案間共享程式碼
  • 依賴管理:明確知道程式碼依賴哪些其他模組
  • 更好的組織:將程式碼按功能分割成小檔案

基本語法

export(匯出)

使用 export 關鍵字將變數、函式或類別匯出,讓其他模組可以使用。

Named Export(具名匯出)

// math.js
export var PI = 3.14159;

export function add(a, b) {
    return a + b;
}

export function multiply(a, b) {
    return a * b;
}

也可以在檔案底部統一匯出:

// math.js
var PI = 3.14159;

function add(a, b) {
    return a + b;
}

function multiply(a, b) {
    return a * b;
}

export { PI, add, multiply };

Default Export(預設匯出)

每個模組只能有一個 default export:

// User.js
export default class User {
    constructor(name) {
        this.name = name;
    }
    
    greet() {
        return 'Hello, ' + this.name;
    }
}
// greeting.js
export default function greet(name) {
    return 'Hello, ' + name;
}

import(匯入)

使用 import 關鍵字從其他模組匯入功能。

匯入 Named Export

// 匯入特定項目
import { add, multiply } from './math.js';

console.log(add(2, 3));       // 5
console.log(multiply(2, 3));  // 6
// 匯入並重新命名
import { add as sum, multiply } from './math.js';

console.log(sum(2, 3));  // 5
// 匯入全部並使用命名空間
import * as math from './math.js';

console.log(math.PI);         // 3.14159
console.log(math.add(2, 3));  // 5

匯入 Default Export

// 匯入 default export,名稱可以自己取
import User from './User.js';
import greet from './greeting.js';

var user = new User('Mike');
console.log(greet('World'));

混合匯入

// 同時匯入 default 和 named exports
import User, { validateEmail, formatName } from './User.js';

在 HTML 中使用模組

在瀏覽器中使用 ES Modules,需要在 <script> 標籤加上 type="module"

<!DOCTYPE html>
<html>
<head>
    <title>ES Modules Demo</title>
</head>
<body>
    <script type="module">
        import { add } from './math.js';
        console.log(add(2, 3));
    </script>
    
    <!-- 或引入外部模組檔案 -->
    <script type="module" src="./main.js"></script>
</body>
</html>
使用 type="module" 的 script 預設是 defer 的,會在 HTML 解析完成後才執行。

模組的特性

模組作用域

模組內的變數預設是私有的,不會污染全域:

// counter.js
var count = 0;  // 私有變數

export function increment() {
    count++;
    return count;
}

export function getCount() {
    return count;
}
// main.js
import { increment, getCount } from './counter.js';

console.log(increment());  // 1
console.log(increment());  // 2
console.log(getCount());   // 2
console.log(count);        // ReferenceError: count is not defined

嚴格模式

模組自動運行在嚴格模式 (strict mode) 下,不需要手動加上 'use strict'

單例模式

模組只會被執行一次,即使被多次 import。這表示模組中的程式碼只會執行一次,狀態會被保留:

// config.js
console.log('config 模組被載入');
export var settings = { theme: 'dark' };
// a.js
import { settings } from './config.js';
settings.theme = 'light';
// b.js
import { settings } from './config.js';
console.log(settings.theme);  // 'light'(被 a.js 修改過了)

config 模組被載入 只會輸出一次。

動態匯入

使用 import() 函式可以動態載入模組,它回傳一個 Promise:

// 條件載入
if (needFeature) {
    import('./feature.js').then(function(module) {
        module.doSomething();
    });
}

// 使用 async/await
async function loadModule() {
    var module = await import('./feature.js');
    module.doSomething();
}

動態匯入適合用於:

  • 按需載入(lazy loading)
  • 根據條件載入不同模組
  • 載入路徑需要動態決定時

重新匯出(Re-export)

可以從一個模組重新匯出另一個模組的內容,常用於建立統一的入口點:

// utils/index.js
export { add, multiply } from './math.js';
export { formatDate } from './date.js';
export { default as User } from './User.js';
// main.js
import { add, formatDate, User } from './utils/index.js';

實際專案結構範例

project/
├── index.html
├── main.js
├── utils/
│   ├── index.js
│   ├── math.js
│   └── string.js
├── components/
│   ├── Button.js
│   └── Modal.js
└── services/
    └── api.js
// utils/math.js
export function add(a, b) {
    return a + b;
}

// utils/string.js
export function capitalize(str) {
    return str.charAt(0).toUpperCase() + str.slice(1);
}

// utils/index.js - 統一匯出
export * from './math.js';
export * from './string.js';

// main.js
import { add, capitalize } from './utils/index.js';

CommonJS vs ES Modules

在 Node.js 環境中,你可能會看到另一種模組語法 CommonJS:

// CommonJS (Node.js 傳統語法)
var math = require('./math');
module.exports = { add: add };

// ES Modules (現代標準)
import { add } from './math.js';
export { add };

主要差異:

特性CommonJSES Modules
載入時機執行時 (runtime)解析時 (parse time)
匯出module.exportsexport
匯入require()import
動態匯入支援需用 import()
瀏覽器支援需打包工具原生支援

現代開發建議使用 ES Modules,它是 JavaScript 的官方標準,且瀏覽器原生支援。

注意事項

  1. 檔案路徑必須完整:在瀏覽器中使用時,import 路徑必須包含副檔名 .js
// 正確
import { add } from './math.js';

// 錯誤(在瀏覽器中)
import { add } from './math';
  1. CORS 限制:使用 file:// 協定開啟 HTML 時,模組會因 CORS 政策而無法載入,需要使用本地伺服器

  2. 循環依賴:模組可以有循環依賴,但要小心處理,避免取得 undefined 值